diff --git a/AGENTS.md b/AGENTS.md index 0cf9faad..13c1843d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Dieses Dokument definiert die Zusammenarbeit zwischen dem User (Owner) und den spezialisierten KI-Agenten. Es dient als zentraler **System-Prompt-Erweiterung** fĂŒr neue Chat-Sessions. -## 🚀 Strategische Ausrichtung (Reality-Reset 28.04.2026) +## 🚀 Strategische Ausrichtung (Reality-Reset 15.06.2026) Das Projekt **"Meldestelle"** entwickelt eine ÖTO/FEI-konforme, offline-fĂ€hige Turnier-Software. 1. **Desktop-First:** PrimĂ€res Ziel ist die Compose Desktop App (KMP). UX & Performance sind auf Profis optimiert. @@ -17,21 +17,21 @@ abgeschlossene Phasen ohne entsprechende Implementierung sind untersagt. Jede Agenten-Antwort **muss** mit dem entsprechenden Badge beginnen, um den Kontext und die Verantwortlichkeit zu klĂ€ren. * **đŸ—ïž [Lead Architect]**: HĂŒter der **MASTER_ROADMAP**. Verantwortlich fĂŒr System-Design, Build-Logik (Gradle), Modulstruktur und ADRs. - * [Playbook](docs/04_Agents/Playbooks/Architect.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/Architect.md) * **📜 [Rulebook Expert]**: WĂ€chter ĂŒber **ÖTO & FEI**. Validiert Business-Rules gegen das offizielle Pferdesport-Regelwerk. - * [Playbook](docs/04_Agents/Playbooks/RulebookExpert.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/RulebookExpert.md) * **đŸ‘· [Backend Developer]**: Kotlin & Spring Boot Experte. Fokus auf DDD, Persistenz (Postgres) und **Delta-Sync APIs**. - * [Playbook](docs/04_Agents/Playbooks/BackendDeveloper.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/BackendDeveloper.md) * **🎹 [Frontend Expert]**: KMP & Compose Desktop Spezialist. Implementiert State-Management und High-Performance UI. - * [Playbook](docs/04_Agents/Playbooks/FrontendExpert.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/FrontendExpert.md) * **đŸ–Œïž [UI/UX Designer]**: "Toolsmith" fĂŒr High-Density Enterprise-UIs. Fokus auf Tastatur-Bedienbarkeit und Effizienz. - * [Playbook](docs/04_Agents/Playbooks/UIUXDesigner.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/UIUXDesigner.md) * **🐧 [DevOps Engineer]**: Infrastruktur-Automatisierung (Docker, Gitea-Actions). Fokus auf StabilitĂ€t und lokale Dev-Umgebung. - * [Playbook](docs/04_Agents/Playbooks/DevOpsEngineer.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/DevOpsEngineer.md) * **🧐 [QA Specialist]**: Test-Stratege (Shift-Left). Fokus auf Unit-, Integration- und Edge-Case-Tests (Testing Pyramid). - * [Playbook](docs/04_Agents/Playbooks/QASpecialist.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/QASpecialist.md) * **đŸ§č [Curator]**: Wissens-Management & Dokumentations-Check (ADR, Reference, Journal). Beendet jede Session. - * [Playbook](docs/04_Agents/Playbooks/Curator.md) + * [Playbook](docs/05_Governance/Agents/Playbooks/Curator.md) ## 2. Der "Meldestelle"-Workflow 1. **Kontext-Check:** Lies immer zuerst die `MASTER_ROADMAP` in `docs/01_Architecture/`. diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index 985f9c32..4ba4e5eb 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -2,12 +2,12 @@ type: Roadmap status: ACTIVE owner: Lead Architect -last_update: 2026-05-06 +last_update: 2026-06-15 --- # MASTER ROADMAP: Meldestelle -đŸ—ïž **[Lead Architect]** | 30. April 2026 +đŸ—ïž **[Lead Architect]** | 15. Juni 2026 **Strategisches Ziel:** Entwicklung einer ÖTO-konformen, offline-fĂ€higen Turnier-Meldestelle als Compose Desktop App (KMP). @@ -17,16 +17,16 @@ VollstĂ€ndige Self-Hosted Infrastruktur (Gitea, Pangolin, Zora). DatensouverĂ€ni - Desktop-App ist der primĂ€re Client (Compose Desktop, KMP) — „Desktop-First“ gilt fĂŒr UX und Architektur. - Offline-First Betrieb mit lokaler Persistenz und opportunistischer Synchronisation. -**Aktueller technischer Stand (30.04.2026):** +**Aktueller technischer Stand (15.06.2026):** * **Infrastruktur:** ✅ "Zora" (MS-R1, ARM64) ist live. Gitea & Registry laufen. * **Networking:** ✅ Pangolin Tunnel ersetzt Cloudflare. * **CI/CD:** ✅ Gitea Actions mit ARM64-Runner (VM 102) aktiv. Docker-Publish Pipeline grĂŒn. * **Code-Basis:** ✅ Backend (Java 25 / Spring Boot / Kotlin), Frontend (KMP/Compose Desktop). * **Domain-Design:** ✅ 6 Bounded Contexts (SCS-Architektur) definiert. Ubiquitous Language erstellt. -* **Domain-Modelle:** ✅ `Reiter`, `DomNennung`, `DomNennungsTransfer`, `Pferd`, `Funktionaer`, `Verein`, - `DomBewerb`, `DomAbteilung`, `DomStartliste`, `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert. +* **Domain-Modelle:** ✅ `Reiter`, `Nennung`, `NennungsTransfer`, `Pferd`, `Funktionaer`, `Verein`, + `Bewerb`, `Abteilung`, `DomStartliste`, `Veranstaltung`, `Turnier`, `Ausschreibung` implementiert. Enums ÖTO-konform. -* **Dokumentation:** ✅ Konsolidiert. ÖTO-Regelwerk-Referenzen (Abteilungs-Schwellenwerte) dokumentiert. +* **Dokumentation:** đŸ§č Sanierung abgeschlossen (5-SĂ€ulen-Struktur). Reality-Reset durchgefĂŒhrt. --- @@ -155,15 +155,15 @@ Code-Stand.* | Dokument | Pfad | |-------------------------------|----------------------------------------------------------------------------------------------| -| Ubiquitous Language | `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` | -| Abteilungs-Schwellenwerte | `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` | -| Warn-Logik-Spezifikation | `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md` | -| Session Log (DDD) | `docs/99_Journal/2026-03-24_Session_Log_DDD_Ubiquitous_Language.md` | -| Infrastruktur | `docs/07_Infrastructure/Zora_System_Architektur.md` | -| Deployment Guide | `docs/07_Infrastructure/Guides/Setup_Git_Deployment_Zora.md` | -| Backup Guide | `docs/07_Infrastructure/Guides/Setup_Backup_Zora.md` | +| Ubiquitous Language | `docs/02_Domain/01_Glossary/Ubiquitous_Language.md` | +| Abteilungs-Schwellenwerte | `docs/02_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` | +| Warn-Logik-Spezifikation | `docs/02_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md` | +| Session Log (DDD) | `docs/05_Governance/Journal/_archive/2026-03-24_Session_Log_DDD_Ubiquitous_Language.md` | +| Infrastruktur | `docs/04_Operations/Infrastructure/Zora_System_Architektur.md` | +| Deployment Guide | `docs/04_Operations/Infrastructure/Guides/Setup_Git_Deployment_Zora.md` | +| Backup Guide | `docs/04_Operations/Infrastructure/Guides/Setup_Backup_Zora.md` | | CI/CD | `.gitea/workflows/docker-publish.yaml` | -| Agent Playbooks | `docs/04_Agents/Playbooks/` | +| Agent Playbooks | `docs/05_Governance/Agents/Playbooks/` | | ADR-Verzeichnis | `docs/01_Architecture/adr/` | | ADR-0025: Plan-USB | `docs/01_Architecture/adr/0025-plan-usb-offline-integritaet.md` | | ADR-0026: Lizenzierung | `docs/01_Architecture/adr/0026-offline-lizenzierung-pay-per-event.md` | diff --git a/docs/02_Domain/00_Glossary.md b/docs/02_Domain/00_Glossary.md new file mode 100644 index 00000000..20fd838a --- /dev/null +++ b/docs/02_Domain/00_Glossary.md @@ -0,0 +1,52 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +last_update: 2026-03-15 +--- +# Glossar der DomĂ€ne "Meldestelle" + +Dieses Dokument definiert die **Ubiquitous Language** (allgegenwĂ€rtige Sprache) des Projekts. Alle Begriffe sind so zu verwenden, wie sie hier definiert sind – sowohl im Code als auch in der Kommunikation. + +## A - E + +* **Abteilung:** Eine Unterteilung eines -> *Bewerbs*. Oft werden Bewerbe mit vielen Startern in mehrere Abteilungen geteilt (z.B. nach Lizenzklasse oder Rasse), die getrennt gewertet werden. +* **Akteur:** Oberbegriff fĂŒr alle Personen (Reiter, Richter, Besitzer) und Organisationen (Vereine), die im System interagieren. +* **Altersklasse (singular):** DomĂ€nenobjekt zur Klassifikation von Teilnehmern nach Alter. SchlĂŒssel-Felder: `altersklasseId` (UUID), `altersklasseCode` (fachlicher SchlĂŒssel, z.B. `JG`, `JR`, `25`, `Y`), `bezeichnung`, optionale Grenzen `minAlter`/`maxAlter`, optionale Filter `sparteFilter`/`geschlechtFilter`. Persistiert in Tabelle `altersklasse` (singular), Spalte `altersklasse_code` (einzigartig). +* **Altersklassen (plural):** Offizielle Altersklassen gemĂ€ĂŸ LIZENZ01.DAT. UnterstĂŒtzte KĂŒrzel: `JG` (Jugendlicher), `JR` (Junior), `25` (U25), `Y` (Junger Reiter). Ein Reiter hat 0..1 Altersklasse aus JG/JR/U25 und optional 0..1 aus Y. +* **Ausschreibung:** Das offizielle Dokument, das alle Bedingungen eines -> *Turniers* festlegt. +* **Bewerb:** Die einzelne sportliche PrĂŒfung (z.B. "SpringprĂŒfung Kl. L"). Kleinste Einheit fĂŒr Nennungen und Ergebnisse. +* **Event:** Der organisatorische Rahmen (z.B. "Pferdefest 2026"), der ein oder mehrere -> *Turniere* beinhalten kann. + +## F - J + +* **FEI-ID:** Eindeutige Identifikationsnummer der Internationalen Reiterlichen Vereinigung (FEI) fĂŒr Reiter und Pferde. +* **Gastreiter:** Ein Reiter mit auslĂ€ndischer StaatsbĂŒrgerschaft, der nicht fĂŒr einen österreichischen Verein startet. +* **Kopfnummer:** + * *National (OEPS):* Die permanente, 4-stellige Registrierungsnummer eines Pferdes beim OEPS (z.B. "A123"). Wird oft am Zaumzeug getragen. + * *International/Turnier:* Eine temporĂ€re Startnummer fĂŒr das spezifische Turnier. + +## K - O + +* **Lebensnummer:** Eine 9-stellige Nummer (bzw. 15-stellig international), die ein Pferd bei der Geburt vom Zuchtverband erhĂ€lt. Dient der eindeutigen Identifizierung, ist aber im OEPS-Kontext bei auslĂ€ndischen Pferden oft generiert und daher nicht zur Suche geeignet. +* **Lizenz (Reiterlizenz):** Die Qualifikationsstufe eines Reiters (z.B. `R1`, `RD1`, `RD2`, `RS2`). Ein Reiter hat 0..1 Reiterlizenz. Wird in der Tabelle `reiterlizenzen` als Stammdaten gefĂŒhrt und am Reiter per `reit_lizenz_id` referenziert. +* **Fahr-Lizenz:** EigenstĂ€ndige Lizenzkategorie fĂŒr den Fahrsport (z.B. `F1`, `F2`). Ein Reiter hat 0..1 Fahr-Lizenz. Stammdaten-Tabelle `fahr_lizenzen`, Referenz am Reiter `fahr_lizenz_id`. +* **Nennung:** Die verbindliche Anmeldung eines Paares (Reiter & Pferd) zu einem -> *Bewerb*. +* **OEPS:** Österreichischer Pferdesportverband. + +## P - T + +* **Reiterlizenzen (Historie):** Optionale Historien-Zuordnungen in `reiter_lizenzen_zuordnung` (z.B. Jahreswechsel). Enthalten den Typ (`REITERLIZENZ`, `STARTKARTE`, `FAHRLIZENZ`), das KĂŒrzel und optional `gĂŒltig_bis`. +* **Satznummer:** + * *Pferd:* 10-stellige, rein numerische ID (z.B. `0000123456`), die ein Pferd in der OEPS-Datenbank eindeutig identifiziert. **PrimĂ€rer SchlĂŒssel fĂŒr den Datenaustausch.** + * *Reiter:* 6-stellige, rein numerische ID fĂŒr Personen. +* **Sperrliste:** Eine vom Verband gefĂŒhrte Liste von Personen oder Pferden, die aktuell nicht startberechtigt sind (meist wegen offener Zahlungen). +* **Startkarte:** Der Nachweis, dass die JahresgebĂŒhr fĂŒr die Lizenz bezahlt wurde. Ohne aktive Startkarte ist (national) kein Start möglich. Stammdaten-Tabelle `startkarten`, Referenz am Reiter `startkarte_id`. +* **Turnier:** Die administrative Einheit (z.B. "CSN-A"), die einem spezifischen Regelwerk (ÖTO oder FEI) unterliegt. +* **Turnier-Kategorie:** Klassifikation eines Turniers (z.B. `CSN-C`, `CSN-C Neu`, `CSN-B`, `CSN-A`, `CDN-C`, `CDN-B`, `CDN-A`). Stammdaten-Tabelle `turnier_kategorien`. +* **Bewerbsklasse:** FrĂŒher „Turnierklasse“. Klassifiziert den Schwierigkeitsgrad eines Bewerbs (Springen: `E`, `A`, `L`, `LM`, `M`, `S`; Dressur: `E` bis `S`). Stammdaten-Tabelle `bewerbs_klassen`. API-Endpunkt: `GET /rules/turnierklassen` liefert dieselben Inhalte (Alias), technisch ĂŒber `RegulationRepository.findAllTurnierklassen()` abgebildet. + +## U - Z + +* **Wertungsserie:** Ein ĂŒbergeordneter Wettbewerb (Cup, Meisterschaft), der Ergebnisse aus mehreren Bewerben/Turnieren aggregiert. +* **ZNS:** Zentrales Nennsystem (bzw. die zugehörigen DatensĂ€tze wie `zns.zip`), ĂŒber das Stammdaten und Nennungen ausgetauscht werden. diff --git a/docs/02_Domain/01_Core_Model/Domain_Model_Veranstaltung_Turnier_Bewerb_Abteilung.md b/docs/02_Domain/01_Core_Model/Domain_Model_Veranstaltung_Turnier_Bewerb_Abteilung.md new file mode 100644 index 00000000..d0b29520 --- /dev/null +++ b/docs/02_Domain/01_Core_Model/Domain_Model_Veranstaltung_Turnier_Bewerb_Abteilung.md @@ -0,0 +1,179 @@ +--- +type: DOMAIN_SPEC +status: ACTIVE +owner: Lead Architect +last_update: 2026-04-02 +--- + +# DomĂ€nen‑Modell: Veranstaltung → Turnier → Bewerb → Abteilung + +Ziel: Dieses Dokument fixiert das offizielle Kern‑Modell fĂŒr die Event‑Struktur sowie Kassa/Konten. Es ist die Single Source of Truth fĂŒr Backend‑Schema, Frontend‑ViewModels und Schnittstellen. + +Quellen/Verweise: +- Ubiquitous Language: `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` +- ÖTO/FEI Referenz: `docs/03_Domain/02_Reference/` (insb. Abteilungs‑Schwellenwerte) +- ADR‑0021 Tenant‑Resolution (Event‑Isolation): `docs/01_Architecture/adr/0021-tenant-resolution-strategy-de.md` + +## 1. Struktur und KardinalitĂ€ten + +Hierarchie und Identifikatoren (kanonisch): + +``` +Veranstaltung (event_id) + ├─ Turnier (tournament_id) [1:N pro Veranstaltung] + │ └─ Bewerb (class_id) [1:N pro Turnier] + │ └─ Abteilung (division_id) [1:N pro Bewerb] + └─ TeilnehmerKonto (account_id) [1:N pro Veranstaltung, referenziert Teilnehmer] + └─ Veranstaltungs‑Kassa (event_cashbox_id = event_id) [1:1] +``` + +Leitlinien: +- Jede Veranstaltung ist ein eigener Tenant (Schema‑per‑Tenant gemĂ€ĂŸ ADR‑0021). +- IDs sind innerhalb des Tenants eindeutig; globale Adressen entstehen durch `{event_id}/{local_id}`. + +## 2. EntitĂ€ten und Aggregate + +### 2.1 Veranstaltung +- SchlĂŒssel: `event_id` (Slug, z. B. `2026-moc-open`) +- Aggregate‑Grenze: umfasst Metadaten der Veranstaltung, Kassa, TeilnehmerKonto‑Katalog. +- Invarianten: + - `status ∈ {draft, active, archived}` + - Archivierte Veranstaltungen sind read‑only. + +### 2.2 Turnier +- SchlĂŒssel: `tournament_id` (innerhalb Veranstaltung eindeutig) +- Attribute (Auszug): Titel, Datum(e), Ort, Status. +- Invarianten: + - Ein Turnier gehört genau zu einer Veranstaltung. + - Löschen nur erlaubt, wenn keine Nennungen/Ergebnisse bestĂ€tigt sind. + +### 2.3 Bewerb +- SchlĂŒssel: `class_id` +- Attribute: Disziplin, Klasse, Lizenzanforderungen, max Starter, Wertungsmodus. +- Invarianten: + - Ein Bewerb gehört genau zu einem Turnier. + - Abteilungsbildung erfolgt gemĂ€ĂŸ Regelwerk/Schwellenwerten. + +### 2.4 Abteilung +- SchlĂŒssel: `division_id` +- Attribute: Lauf‑/Startzeit, Parcours/Bahn, Typ (siehe unten), Ergebnisstatus. +- Invarianten: + - Eine Abteilung gehört genau zu einem Bewerb. + - Typen steuern UI, Zeitplan und Preisgeld-/Siegerehrungslogik. + +### 2.5 TeilnehmerKonto (auf Veranstaltungsebene) +- Zweck: Vereinheitlichte finanzielle Sicht je Teilnehmer ĂŒber mehrere Turniere derselben Veranstaltung (Multi‑Turnier). +- SchlĂŒssel: `account_id` +- Beziehungen: + - `Teilnehmer` (z. B. Reiter, Verein, Team) 1:1 ↔ TeilnehmerKonto (pro Veranstaltung) + - Buchungen entstehen aus Nennungen, Startgeldern, GebĂŒhren, Gutschriften, RĂŒckzahlungen turnierĂŒbergreifend. +- Invarianten: + - Ein Teilnehmer hat höchstens ein Konto pro Veranstaltung. + - Saldo ist Summe aller bestĂ€tigten Buchungen innerhalb des Tenants. + +### 2.6 Veranstaltungs‑Kassa (Turnierâ€‘ĂŒbergreifender Saldo) +- Zweck: Aggregierte Kasse der gesamten Veranstaltung; spiegelt Einzahlungen/Auszahlungen und Summen ĂŒber alle Turniere. +- SchlĂŒssel: `event_cashbox_id` = `event_id` +- Komponenten: + - Journal (Belege): Ein/Auszahlungen, Umbuchungen, Korrekturen. + - Summen: aktueller Bestand, Reserven, offene Posten (aggregiert aus TeilnehmerKonten). +- Invarianten: + - Jede Buchung betrifft genau ein Gegenkonto (TeilnehmerKonto oder internes Konto). + - Journal ist unverĂ€nderlich; Korrekturen erfolgen als Gegenbuchung. + +## 3. Abteilungs‑Typen + +Definiert als `enum DivisionType`: +- `STANDARD`: Normale Abteilung mit regulĂ€rer Siegerehrung innerhalb des Bewerbs. +- `SEPARATE_SIEGEREHRUNG`: Abteilung, deren Siegerehrung separat organisiert wird (z. B. zusammengelegt/zeitlich entkoppelt) — STATUS: vorlĂ€ufig, Detailregeln folgen durch 📜 Rulebook Expert. +- `ORGANISATORISCH`: Rein organisatorische Abteilung (z. B. Aufteilung aus Zeit/Platz‑GrĂŒnden), ohne eigenstĂ€ndige sportliche Wertung/Preisgeldlogik. + +Hinweis: Die genaue Ausgestaltung von `SEPARATE_SIEGEREHRUNG` (Preisgeld‑Aggregation, Ranking‑Anzeige, Protokoll) wird im Rulebook‑Dokument ergĂ€nzt und kann weitere Felder/Beziehungen erfordern (z. B. Verweis auf „gemeinsame Siegerehrung fĂŒr Bewerbe X/Y“). + +## 4. Datenmodell‑Skizze (relationale Sicht je Tenant) + +```sql +-- Veranstaltung (im Tenant‑Schema) +CREATE TABLE event ( + event_id TEXT PRIMARY KEY, + title TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('draft','active','archived')) +); + +CREATE TABLE tournament ( + tournament_id TEXT PRIMARY KEY, + event_id TEXT NOT NULL REFERENCES event(event_id), + title TEXT NOT NULL, + start_date DATE, + end_date DATE, + status TEXT NOT NULL +); + +CREATE TABLE class ( + class_id TEXT PRIMARY KEY, + tournament_id TEXT NOT NULL REFERENCES tournament(tournament_id), + discipline TEXT NOT NULL, + level TEXT NOT NULL, + max_starters INT, + scoring_mode TEXT NOT NULL +); + +CREATE TABLE division ( + division_id TEXT PRIMARY KEY, + class_id TEXT NOT NULL REFERENCES class(class_id), + type TEXT NOT NULL CHECK (type IN ('STANDARD','SEPARATE_SIEGEREHRUNG','ORGANISATORISCH')), + scheduled_at TIMESTAMP, + status TEXT NOT NULL +); + +-- TeilnehmerKonto (veranstaltungsweit) +CREATE TABLE participant_account ( + account_id TEXT PRIMARY KEY, + event_id TEXT NOT NULL REFERENCES event(event_id), + participant_ref TEXT NOT NULL, -- verweist auf Teilnehmer‑Stammdatensatz im Tenant + UNIQUE(event_id, participant_ref) +); + +CREATE TABLE participant_ledger_entry ( + entry_id TEXT PRIMARY KEY, + account_id TEXT NOT NULL REFERENCES participant_account(account_id), + booking_ts TIMESTAMP NOT NULL, + amount_cents BIGINT NOT NULL, + currency TEXT NOT NULL DEFAULT 'EUR', + source TEXT NOT NULL, -- z. B. Nennung, Startgeld, RĂŒckzahlung + tournament_id TEXT NULL REFERENCES tournament(tournament_id) +); + +-- Veranstaltungs‑Kassa +CREATE TABLE event_cashbox ( + event_cashbox_id TEXT PRIMARY KEY REFERENCES event(event_id), + created_at TIMESTAMP NOT NULL +); + +CREATE TABLE cashbox_journal ( + journal_id TEXT PRIMARY KEY, + event_cashbox_id TEXT NOT NULL REFERENCES event_cashbox(event_cashbox_id), + booking_ts TIMESTAMP NOT NULL, + amount_cents BIGINT NOT NULL, + direction TEXT NOT NULL CHECK (direction IN ('IN','OUT')), + counterparty TEXT NOT NULL, -- account_id oder internes Konto + memo TEXT +); +``` + +## 5. Invarianten und GeschĂ€ftsregeln (Auszug) +- Abteilungs‑Typ `ORGANISATORISCH` darf keine eigenstĂ€ndige Preisgeldlogik auslösen. +- `SEPARATE_SIEGEREHRUNG` kann Ergebnisse bĂŒndeln/verschieben; Detailregeln werden im Rulebook spezifiziert. Bis dahin bleiben API‑Felder stabil, Verhalten konservativ (keine automatische Zusammenlegung ohne explizite VerknĂŒpfung). +- TeilnehmerKonto‑Saldo = Summe aller bestĂ€tigten `participant_ledger_entry.amount_cents`. +- Event‑Kassa‑Bestand = Summe `IN` − Summe `OUT`; regelmĂ€ĂŸige Abstimmung mit Summe aller Teilnehmer‑Offenen Posten. + +## 6. API/DTO Richtlinien (High‑Level) +- Alle API‑Ressourcen werden unterhalb des Tenants adressiert (Header `X-Event-Id`). +- DTOs tragen stabile `*_id` Felder entsprechend diesem Modell; Referenzen sind per ID, keine eingebetteten Aggregate außer Read‑Views. +- Enum `DivisionType` wird exakt wie oben benannt; neue Typen erfordern Versionserhöhung des Schemas. + +## 7. ToDos und Folgearbeiten +- 📜 Rulebook Expert: Detail‑Spezifikation `SEPARATE_SIEGEREHRUNG` (Preisgeld, Ranking, UI‑Hinweise) ergĂ€nzen. +- đŸ§č Curator: `Ubiquitous_Language.md` um obige Begriffe/Definitionen erweitern. +- đŸ‘· Backend: Schema‑Migrationen pro Tenant gemĂ€ĂŸ obiger Tabellen; Repositories/Services entsprechend zuschneiden. +- 🎹 Frontend: ViewModels/Stores entlang dieser Struktur aktualisieren (Navigation: Veranstaltung → Turnier → Bewerb → Abteilung). diff --git a/docs/02_Domain/01_Core_Model/Entities/Database_Schema.sql b/docs/02_Domain/01_Core_Model/Entities/Database_Schema.sql new file mode 100644 index 00000000..a3e2e4a0 --- /dev/null +++ b/docs/02_Domain/01_Core_Model/Entities/Database_Schema.sql @@ -0,0 +1,241 @@ +-- Database Schema Draft for Meldestelle (Offline-First) +-- Dialect: SQLite (compatible with SQLDelight) +-- Status: Draft / Proposal +-- Based on: OEPS Legacy Spec V2.4 & Domain Analysis + +-- ================================================================== +-- 1. CORE INFRASTRUCTURE (Sync & Audit) +-- ================================================================== +-- Every table should ideally have these fields, but for brevity +-- they are implied or added where critical. +-- id: TEXT NOT NULL PRIMARY KEY (UUID) +-- created_at: INTEGER NOT NULL (Epoch Millis) +-- updated_at: INTEGER NOT NULL (Epoch Millis) +-- version: INTEGER NOT NULL (Optimistic Locking / Sync Counter) +-- is_deleted: INTEGER NOT NULL DEFAULT 0 (Soft Delete) + +-- ================================================================== +-- 2. MASTER DATA (Stammdaten) +-- ================================================================== + +-- Akteure: Personen und Organisationen +-- Covers: Reiter, Richter, Besitzer, Vereine +CREATE TABLE actor ( + id TEXT NOT NULL PRIMARY KEY, + type TEXT NOT NULL, -- 'PERSON', 'ORGANIZATION' + + -- Display Data + first_name TEXT, -- NULL for Organizations + last_name TEXT NOT NULL, -- Name or Org-Name + + -- OEPS Specifics (Legacy Spec) + oeps_id TEXT, -- 'Satznummer' (6 digits for Person, 4 for Club) + oeps_category TEXT, -- 'Verein', 'Reiter', 'Richter' + + -- Licenses & Status + license_code TEXT, -- e.g. 'R1', 'RD3' + has_start_card INTEGER NOT NULL DEFAULT 0, -- Boolean: Paid annual fee? + is_locked INTEGER NOT NULL DEFAULT 0, -- Boolean: Sperrliste? + + -- Contact & Meta + nationality TEXT NOT NULL DEFAULT 'AUT', -- ISO 3-Letter + contact_json TEXT, -- Address, Phone, Email + + -- Sync Meta + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1 +); + +CREATE INDEX idx_actor_oeps_id ON actor(oeps_id); +CREATE INDEX idx_actor_name ON actor(last_name, first_name); + + +-- Pferde +CREATE TABLE horse ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + + -- Identification + oeps_id TEXT, -- 'Satznummer' (10 digits) - CRITICAL for Export + head_number_permanent TEXT, -- 'Kopfnummer' (e.g. A123) + life_number TEXT, -- 'Lebensnummer' (Zucht) + fei_id TEXT, + + -- Details + birth_year INTEGER, + gender TEXT, -- 'M', 'W', 'G' (Gelding/Wallach) + color TEXT, + sire_name TEXT, -- Vater (Denormalized for search) + dam_name TEXT, -- Mutter + + -- Owner Link + owner_id TEXT, -- FK to actor.id + + -- Status + is_locked INTEGER NOT NULL DEFAULT 0, -- Sperrliste + + -- Sync Meta + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1 +); + +CREATE INDEX idx_horse_oeps_id ON horse(oeps_id); +CREATE INDEX idx_horse_head_num ON horse(head_number_permanent); +CREATE INDEX idx_horse_name ON horse(name); + +-- ================================================================== +-- 3. EVENT STRUCTURE +-- ================================================================== + +CREATE TABLE event ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + start_date INTEGER NOT NULL, -- Epoch Day + end_date INTEGER NOT NULL, + location TEXT, + organizer_id TEXT NOT NULL, -- FK to actor.id + + status TEXT NOT NULL DEFAULT 'PLANNING' -- PLANNING, ACTIVE, ARCHIVED +); + +CREATE TABLE tournament ( + id TEXT NOT NULL PRIMARY KEY, + event_id TEXT NOT NULL REFERENCES event(id), + + -- OEPS Spec + oeps_number TEXT NOT NULL, -- 5 digits (e.g. 21001) + category TEXT, -- e.g. 'CSN-A' + ruleset TEXT NOT NULL DEFAULT 'OETO', -- 'OETO', 'FEI' + + -- Sync Meta + updated_at INTEGER NOT NULL +); + +-- Bewerbe (Competitions) +-- Note: If a competition is split into 2 departments (Abteilungen), +-- we create 2 rows here to match the OEPS 'B-Satz' logic. +CREATE TABLE competition ( + id TEXT NOT NULL PRIMARY KEY, + tournament_id TEXT NOT NULL REFERENCES tournament(id), + + -- Identification + code_internal TEXT NOT NULL, -- '01', '02' (2 digits) + code_official TEXT, -- '001' (3 digits, optional) + division_id INTEGER NOT NULL DEFAULT 0, -- 'Abteilung' (0=None, 1=1st, 2=2nd) + + -- Description + title TEXT NOT NULL, + category TEXT, -- e.g. 'LM', 'S*' + discipline TEXT NOT NULL, -- 'D', 'S', 'C' (Dressage, Jumping, Combined) + + -- Rules & Scoring + scoring_method TEXT NOT NULL, -- 'A0', 'C', 'DRESSAGE_PERCENT' + start_fee INTEGER NOT NULL DEFAULT 0, -- In Cents + + -- State + status TEXT NOT NULL DEFAULT 'OPEN', -- OPEN, CLOSED_FOR_ENTRIES, RUNNING, FINISHED, SIGNED_OFF + + -- Sync Meta + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_comp_tournament ON competition(tournament_id); + +-- ================================================================== +-- 4. SPORT & PROCESS +-- ================================================================== + +-- Nennungen (Entries) +-- Represents the intent to start. +CREATE TABLE entry ( + id TEXT NOT NULL PRIMARY KEY, + competition_id TEXT NOT NULL REFERENCES competition(id), + + -- The Pair + horse_id TEXT NOT NULL REFERENCES horse(id), + rider_id TEXT NOT NULL REFERENCES actor(id), + + -- Financials + responsible_person_id TEXT REFERENCES actor(id), -- Who pays? + fee_agreed INTEGER NOT NULL, -- In Cents (Snapshot of price at entry time) + payment_status TEXT NOT NULL DEFAULT 'PENDING', -- PENDING, PAID, WAIVED + + -- Validation Override (The "Human Factor") + validation_status TEXT NOT NULL DEFAULT 'OK', -- OK, WARNING, BLOCKED + override_comment TEXT, -- Why was this allowed despite warning? + + -- Sync Meta + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_entry_comp ON entry(competition_id); +CREATE INDEX idx_entry_rider ON entry(rider_id); + +-- Startliste (Start Order) +-- Subset of entries that actually start. +CREATE TABLE start_list_entry ( + id TEXT NOT NULL PRIMARY KEY, + entry_id TEXT NOT NULL REFERENCES entry(id), + + -- Ordering + start_order INTEGER, -- 1, 2, 3... + start_time_planned INTEGER, -- Epoch Millis (optional) + + -- Tournament Specifics + head_number_event TEXT, -- Startnummer am Turnier (kann von A123 abweichen) + + -- Status + status TEXT NOT NULL DEFAULT 'READY', -- READY, STARTED, DNS (Did Not Start) + + UNIQUE(entry_id) +); + +-- Ergebnisse (Results) +CREATE TABLE result ( + id TEXT NOT NULL PRIMARY KEY, + start_list_entry_id TEXT NOT NULL REFERENCES start_list_entry(id), + + -- The Outcome + rank INTEGER, -- 1, 2, 3... (NULL if eliminated) + + -- Scoring Details (Polymorphic based on Competition Type) + points_jump_faults DECIMAL(5,2), -- Springfehler + time_taken_ms INTEGER, -- Zeit in Millisekunden + score_dressage_percent DECIMAL(5,3), -- 72.500 + score_dressage_total DECIMAL(6,2), -- Summe Punkte + + -- Status Flags + classification TEXT NOT NULL DEFAULT 'OK', -- OK, EL (Elim), RET (Retired), DIS (Disq) + + -- Detailed Marks (JSON) + -- e.g. { "judge_C": 7.5, "judge_H": 7.2, "obstacles": [...] } + details_json TEXT, + + -- Money + prize_money INTEGER DEFAULT 0, -- In Cents + + -- Audit + updated_by_user_id TEXT, + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_result_starter ON result(start_list_entry_id); + +-- ================================================================== +-- 5. AUDIT LOG (NFR-07) +-- ================================================================== + +CREATE TABLE audit_log ( + id TEXT NOT NULL PRIMARY KEY, + entity_type TEXT NOT NULL, -- 'RESULT', 'ENTRY' + entity_id TEXT NOT NULL, + action TEXT NOT NULL, -- 'CREATE', 'UPDATE', 'DELETE' + + user_id TEXT, + timestamp INTEGER NOT NULL, + + changes_json TEXT -- { "score_old": 7.0, "score_new": 7.5 } +); diff --git a/docs/02_Domain/01_Core_Model/Entities/Event_Structure_Diagram.md b/docs/02_Domain/01_Core_Model/Entities/Event_Structure_Diagram.md new file mode 100644 index 00000000..e5de59f8 --- /dev/null +++ b/docs/02_Domain/01_Core_Model/Entities/Event_Structure_Diagram.md @@ -0,0 +1,177 @@ +# Turnier- & Bewerbsstruktur Diagramm + +Dieses Diagramm zeigt die strukturelle Hierarchie und die Beziehungen zwischen Event, Turnier und Bewerb basierend auf +dem ÖTO-Regelwerk und den Anforderungen der OEPS-Schnittstelle. + +```mermaid +erDiagram +%% Entities + EVENT { + string id PK "UUID" + string name "z.B. Apropos Pferd 2026" + date start_date + date end_date + string location + string organizer_id FK "Veranstalter (Akteur)" + string status "PLANNING, ACTIVE, ARCHIVED" + } + + TOURNAMENT { + string id PK "UUID" + string event_id FK + string oeps_number "z.B. 21001 (A-Satz)" + string category "z.B. CSN-A, CDN-B, CDN-C-NEU" + string ruleset "OETO oder FEI" + } + + COMPETITION { + string id PK "UUID" + string tournament_id FK + string code_internal "2-stellig (B-Satz Pos. 2)" + string code_official "3-stellig (B-Satz Pos. 61)" + int division_id "Abteilung (0=keine, 1=Abt. 1...)" + string title "z.B. StandardspringprĂŒfung" + string discipline "S (Springen), D (Dressur), C" + string category "Klasse z.B. A, L, M, S*, lizenzfrei" + string scoring_method "Richtverfahren (z.B. A0, A2, AM5, Stil)" + int start_fee "in Cent" + string status "OPEN, RUNNING, FINISHED" + int planned_duration_per_starter_seconds "Kalkulierte Reitzeit pro Starter" + } + + OFFICIALS { + string id PK "UUID" + string tournament_id FK "oder competition_id" + string actor_id FK "Richter / Parcoursbauer" + string role "Hauptrichter, Richter, TD, Parcoursbauer" + } + + ACTOR { + string id PK "UUID" + string oeps_id "z.B. 123456" + string type "PERSON, ORGANIZATION" + string name "VollstĂ€ndiger Name" + string license_code "z.B. R1" + } + + QUALIFICATION { + string id PK "UUID" + string actor_id FK + string discipline "S, D, V" + string level "z.B. L, M, S" + string role "RICHTER, PARCOURSBAUER" + } + + ENTRY { + string id PK "UUID" + string competition_id FK + string horse_id FK + string rider_id FK + string status "ACTIVE, WITHDRAWN" + string preferred_start_position "Optional: vorne, hinten, zu" + } + + START_LIST_ENTRY { + string id PK "UUID" + string entry_id FK + int start_order "Nummer in der Startreihenfolge" + string status "READY, IN_PROGRESS, FINISHED, DNS" + } + + RESULT { + string id PK "UUID" + string start_list_entry_id FK + int rank "Platzierung (kann manuell ĂŒberschrieben werden)" + string status "OK, EL, RET, DNS" + float points "Fehlerpunkte / Wertnote" + float time_seconds "Benötigte Zeit" + int prize_money "Ausbezahltes Geld in Cent" + } + + BILLING_ACCOUNT { + string id PK "UUID" + string event_id FK + string payer_id FK "Akteur (Zahler)" + int current_balance "in Cent" + } + + TRANSACTION { + string id PK "UUID" + string account_id FK + string entry_id FK "Optionaler Bezug zur Nennung" + string type "NENNGELD, STARTGELD, TAUSCHGEBÜHR, NACHNENNUNG" + int amount "in Cent" + } + +%% Relationships + EVENT ||--o{ TOURNAMENT: "beinhaltet" + TOURNAMENT ||--o{ COMPETITION: "besteht aus (B-Satz)" + TOURNAMENT ||--o{ OFFICIALS: "hat FunktionĂ€re (C-Satz)" + COMPETITION ||--o{ OFFICIALS: "wird gerichtet von" + OFFICIALS }o--|| ACTOR: "ist ein" + ACTOR ||--o{ QUALIFICATION: "hat Berechtigungen" + COMPETITION ||--o{ ENTRY: "hat Nennungen" + ENTRY ||--o| START_LIST_ENTRY: "wird zu Starter" + START_LIST_ENTRY ||--o| RESULT: "hat Ergebnis" + EVENT ||--o{ BILLING_ACCOUNT: "verwaltet Kassa fĂŒr" + BILLING_ACCOUNT ||--o{ TRANSACTION: "verbucht" +``` + +## ErlĂ€uterung zum Modell + +### 1. `EVENT` (Veranstaltung) + +Das **Event** ist der ĂŒbergeordnete organisatorische Rahmen (z.B. "Pferdefestival 2026"). Es hat ein Datum, einen Ort +und einen Veranstalter. Es existiert unabhĂ€ngig von den strikten OEPS-Regularien und ist primĂ€r fĂŒr die administrative +Verwaltung (Rechnungsstellung, Anlagenplanung) wichtig. + +### 2. `TOURNAMENT` (Turnier) +Ein Event kann mehrere **Turniere** beinhalten (z.B. ein nationales CSN-B und gleichzeitig ein internationales CSI2*). +Das Turnier ist die Instanz, die strikt an das Regelwerk gebunden ist: +* Es korrespondiert 1:1 mit dem **A-Satz** des OEPS-Pflichtenhefts. +* Es hat eine eindeutige 5-stellige `oeps_number`. +* Es legt fest, nach welchem Regelwerk (ÖTO vs. FEI) geritten und ausgewertet wird. In der ersten Phase konzentrieren + wir uns auf **C-NEU** und **C**. + +### 3. `COMPETITION` (Bewerb / PrĂŒfung) + +Die kleinste sportliche Einheit und das HerzstĂŒck der Ausschreibung. Wir fokussieren uns initial auf **Dressur (D)** und +**Springen (S)**. +Hier finden wir die direkte Verbindung zum **B-Satz** der Legacy-Spezifikation: + +* **Abteilungen (`division_id`):** Laut OEPS-Schnittstelle wird jede Abteilung (z.B. R1-Reiter getrennt von R2-Reitern) + datentechnisch fast wie ein eigener Bewerb behandelt. In unserer Datenbank reprĂ€sentieren wir jede Abteilung als + eigene Zeile in der Tabelle `competition` (oder verknĂŒpfen sie intelligent), da jede Abteilung ihre eigene + Ergebnisliste und ihr eigenes Preisgeld hat. +* **Nummerierung:** Intern 2-stellig (`code_internal`), fĂŒr Turniere ab 100 Bewerben offiziell 3-stellig ( + `code_official` an Stelle 61). +* **Richtverfahren:** Entscheidet darĂŒber, wie Fehler und Zeit (Springen) oder Wertnoten (Dressur) in ein Ranking + ĂŒbersetzt werden. +* **Zeitplanung:** `planned_duration_per_starter_seconds` ist fĂŒr die Kalkulation der Startzeiten elementar. + +### 4. `OFFICIALS` & `QUALIFICATION` (Der C-Satz & ZNS-Import) + +Dies entspricht dem **C-Satz**. Richter und Parcoursbauer mĂŒssen einem Turnier oder spezifisch einem Bewerb zugewiesen +werden. + +* Neu: Die EntitĂ€t `QUALIFICATION` bildet die importierten Lizenzstufen aus der ZNS-Datei `RICHT01.DAT` ab. Das Backend + gleicht diese Berechtigungen bei der Zuweisung gegen die Kategorie der `COMPETITION` ab. + +### 5. `ENTRY`, `START_LIST_ENTRY` & `RESULT` (Nennung bis Ergebnis) + +* **`ENTRY` (Nennung):** Die Nennung verknĂŒpft ein Pferd und einen Reiter mit einem bestimmten Bewerb. Hier werden auch + **StartwĂŒnsche** (vorne/hinten) erfasst, die fĂŒr den Telefon-Workflow kritisch sind. +* **`START_LIST_ENTRY` (Startliste):** Generiert aus den aktiven Nennungen. Definiert die exakte Startreihenfolge. +* **`RESULT` (Ergebnis):** HĂ€lt die im "Richterturm-Workflow" erfassten Rohdaten (Punkte/Zeit) und die errechnete + Platzierung. Die Platzierung kann bei Bedarf manuell vom Veranstalter ĂŒberschrieben werden (z.B. wenn mehr Platziert + werden sollen, als die ÖTO vorschlĂ€gt). + +### 6. Billing Context (`BILLING_ACCOUNT` & `TRANSACTION`) + +Die Abrechnung (Kassa) wird als separater Bereich an das `EVENT` gehĂ€ngt. + +* Es wird ein `BILLING_ACCOUNT` pro "Zahler" (meist ein `ACTOR`) gefĂŒhrt. +* Jede GebĂŒhr (Nenngeld, Startgeld, NachnenngebĂŒhr) wird als atomare `TRANSACTION` verbucht. +* Wenn ein **Nennungstausch** stattfindet, bleibt das Nenngeld als positive Transaktion auf dem Account erhalten, + wĂ€hrend fĂŒr die neue Nennung lediglich eine neue Startgeld- oder TauschgebĂŒhr-Transaktion erzeugt wird. Das ermöglicht + maximale FlexibilitĂ€t. diff --git a/docs/02_Domain/01_Core_Model/Entities/Overview.md b/docs/02_Domain/01_Core_Model/Entities/Overview.md new file mode 100644 index 00000000..c6cec05a --- /dev/null +++ b/docs/02_Domain/01_Core_Model/Entities/Overview.md @@ -0,0 +1,141 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +--- +# 01 - Core Domain Entities + +Dieses Dokument definiert die zentralen fachlichen EntitĂ€ten (Kern-EntitĂ€ten) des "Meldestelle"-Projekts. Diese EntitĂ€ten bilden das Fundament des Datenmodells und der gesamten Anwendungslogik. + +> **Hinweis:** Dieses Modell wurde basierend auf der Analyse des OEPS-Pflichtenhefts 2021 V2.4 verfeinert. + +## Die 6 Kern-EntitĂ€ten + +1. **Event**: Der organisatorische Rahmen. +2. **Turnier**: Die administrative, regelbasierte Einheit. +3. **Bewerb**: Die einzelne sportliche PrĂŒfung. +4. **Wertungsserie**: Der ĂŒbergeordnete Cup oder die Meisterschaft. +5. **Akteur**: Personen und Organisationen. +6. **Pferd**: Die Pferde als eigenstĂ€ndige EntitĂ€t. + +--- + +### 1. EntitĂ€t: `Veranstaltung` (Event) + +**Zweck:** Der ĂŒbergeordnete organisatorische Container fĂŒr eine Veranstaltung an einem bestimmten Ort und zu einer bestimmten Zeit. Eine Veranstaltung kann ein oder mehrere Turniere umfassen. + +**Beispiele:** "Apropos Pferd 2026", "Vereinsturnier Reitclub XY". + +**Attribute:** +* `Veranstaltung-ID` (PK): Eindeutiger technischer SchlĂŒssel (UUID). +* `Name`: Offizieller Name der Veranstaltung. +* `Veranstaltungsort`: Adresse und Name der Anlage. +* `Datum_Von`: Startdatum des Events. +* `Datum_Bis`: Enddatum des Events. +* `Veranstalter_ID` (FK): Verweis auf den `Akteur`, der die Veranstaltung ausrichtet. +* `Sparten`: Liste der angebotenen Sparten. +* `AustragungsplĂ€tze`: Liste der AustragungsplĂ€tze (`austragungsplaetze`). +* `Artikel-Preisliste`: Liste der Zusatzartikel inkl. Preise (`artikelPreisliste`). +* `Status`: Grober Zustand des Events (z.B. `In Planung`, `Laufend`, `Abgeschlossen`). + +--- + +### 2. EntitĂ€t: `Turnier` + +**Zweck:** Definiert eine administrative Einheit innerhalb eines Events, die unter einem einheitlichen Regelwerk stattfindet. Hier werden Nennungen, Starter- und Ergebnislisten verwaltet. + +**Beispiele:** "CSN-A im Rahmen der Apropos Pferd", "CSI2* im Rahmen der Apropos Pferd". + +**Attribute:** +* `Turnier-ID` (PK): Eindeutiger technischer SchlĂŒssel (UUID). +* `Veranstaltung_ID` (FK): Verweis auf die ĂŒbergeordnete `Veranstaltung`. +* `Turniernummer_OEPS`: 5-stellige Nummer (z.B. `21001`) fĂŒr den Datenaustausch. +* `Reglement`: Entscheidende Weiche fĂŒr die Anwendungslogik (Enum: `OETO`, `FEI`). +* `Kategorie`: Offizielle Turnierkategorie (z.B. "CSN-A", "CSI2*", "CDI-W"). +* `Sparte`: Sparte des Turniers (z.B. `Springen`, `Dressur`). +* `Turnierbeauftragter_ID` (FK): Referenz auf den Turnierbeauftragten (TB). +* `Ausschreibung_Text`: Der vollstĂ€ndige Text der Ausschreibung. +* `Nennschluss`: Datum und Uhrzeit. +* `NachnenngebuehrVerlangt`: Flag, ob NachnenngebĂŒhr erhoben wird. +* `NenntauschboerseAktiv`: Flag, ob Nenntauschbörse aktiv ist. +* `Status`: Detaillierter Zustand des Turniers (z.B. `Genehmigt`, `Nennschluss`, `Ergebnisse final`). + +--- + +### 3. EntitĂ€t: `Bewerb` + +**Zweck:** Die einzelne sportliche PrĂŒfung innerhalb eines Turniers. Ein Bewerb ist die kleinste Einheit, fĂŒr die eine Nennung möglich ist und eine Ergebnisliste erstellt wird. + +**Beispiele:** "StandardspringprĂŒfung Kl. L", "DressurprĂŒfung Kl. M - Aufgabe M5". + +**Attribute:** +* `Bewerb-ID` (PK): Eindeutiger technischer SchlĂŒssel (UUID). +* `Turnier_ID` (FK): Verweis auf das zugehörige `Turnier`. +* `Nummer_Intern`: 2-stellige Nummer (z.B. `05`). +* `Nummer_Offiziell`: 3-stellige Nummer (z.B. `005`) fĂŒr Turniere > 99 Bewerbe. +* `Abteilung`: Kennzeichen fĂŒr Unterteilungen (z.B. `1`, `2`). Default `0`. +* `Titel`: Der offizielle Titel des Bewerbs. +* `Startgeld`: Das fĂŒr diesen Bewerb zu entrichtende Startgeld (in EUR). +* `Startberechtigung_Text`: Textuelle Beschreibung der Teilnahmevoraussetzungen. +* `Besondere_Bestimmungen`: Spezielle Regeln nur fĂŒr diesen Bewerb. + +--- + +### 4. EntitĂ€t: `Wertungsserie` + +**Zweck:** Definiert eine ĂŒbergeordnete Wertung (Cup, Meisterschaft), die Ergebnisse aus spezifischen Bewerben ĂŒber mehrere Turniere hinweg sammelt und nach einem eigenen Regelwerk auswertet. + +**Beispiele:** "Casino Grand Prix 2026", "OÖ Landesmeisterschaft Dressur Allgemeine Klasse". + +**Attribute:** +* `Serie-ID` (PK): Eindeutiger technischer SchlĂŒssel (UUID). +* `Name`: Offizieller Name der Serie. +* `Saison`: Das Jahr, in dem die Serie stattfindet. +* `Reglement_Text`: Die spezifischen Regeln fĂŒr die Wertung (Punktesystem, etc.). +* `Teilnahmeberechtigung_Text`: Regeln, wer an der Serie teilnehmen darf. +* `Qualifikationsbewerbe`: Eine Liste von Verweisen auf die `Bewerb-IDs`, deren Ergebnisse fĂŒr diese Serie gewertet werden. + +--- + +### 5. EntitĂ€t: `Akteur` + +**Zweck:** Zentrale, widerspruchsfreie Verwaltung aller beteiligten Personen und Organisationen. + +**Beispiele:** Ein Reiter, ein Pferdebesitzer, ein ZĂŒchter, ein Richter, ein Reitverein. + +**Attribute:** +* `Akteur-ID` (PK): Eindeutiger technischer SchlĂŒssel (UUID). +* `Typ`: `PERSON` oder `ORGANISATION`. +* `Name`: VollstĂ€ndiger Name der Person oder Organisation. +* `Kontakt`: Adress- und Kontaktdaten. +* `Rollen`: Liste der Rollen (z.B. `REITER`, `RICHTER`). +* **OEPS-Daten (fĂŒr Personen):** + * `Satznummer`: 6-stellig, numerisch (PrimĂ€rschlĂŒssel OEPS). + * `Lizenz`: Aktueller Lizenzcode (z.B. "R1"). + * `Startkarte`: Boolean/Status (JahresgebĂŒhr bezahlt?). + * `Verein_ID`: Verweis auf den Stammverein. +* **Identifikatoren (Sonstige):** + * `FEI-ID` + * `Mitgliedsnummer_Zuchtverband` + +--- + +### 6. EntitĂ€t: `Pferd` + +**Zweck:** Zentrale Verwaltung aller Pferde, egal ob im Sport oder in der Zucht. + +**Beispiele:** Ein international erfolgreiches Sportpferd, eine Zuchtstute. + +**Attribute:** +* `Pferd-ID` (PK): Eindeutiger technischer SchlĂŒssel (UUID). +* `Name`: Offizieller Name des Pferdes. +* `Abstammung_Vater_ID` (FK): Verweis auf ein anderes `Pferd` (Vater). +* `Abstammung_Mutter_ID` (FK): Verweis auf ein anderes `Pferd` (Mutter). +* `Besitzer_ID` (FK): Verweis auf den `Akteur`, dem das Pferd gehört. +* **OEPS-Daten:** + * `Satznummer`: 10-stellig, numerisch (PrimĂ€rschlĂŒssel OEPS). + * `Kopfnummer`: 4-stellig, alphanumerisch (Permanente ID). + * `Lebensnummer`: 9-stellig (Zuchtnummer). +* **FEI-Daten:** + * `FEI-ID`: Eindeutige FEI-Nummer. + * `FEI-Pass`: Passnummer (kann abweichen). diff --git a/docs/02_Domain/01_Core_Model/Entities/README.md b/docs/02_Domain/01_Core_Model/Entities/README.md new file mode 100644 index 00000000..a2515d75 --- /dev/null +++ b/docs/02_Domain/01_Core_Model/Entities/README.md @@ -0,0 +1,11 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +--- +# EntitĂ€ten des Kern-Modells + +Dieses Verzeichnis enthĂ€lt detaillierte Beschreibungen der zentralen fachlichen EntitĂ€ten des "Meldestelle"-Projekts. +Jede Datei beschreibt eine EntitĂ€t und ihre Attribute. + +Diese Dokumente sind die "Wahrheit" fĂŒr die Implementierung. diff --git a/docs/02_Domain/01_Core_Model/Processes/ZNS_Import_Process.md b/docs/02_Domain/01_Core_Model/Processes/ZNS_Import_Process.md new file mode 100644 index 00000000..615bd086 --- /dev/null +++ b/docs/02_Domain/01_Core_Model/Processes/ZNS_Import_Process.md @@ -0,0 +1,101 @@ +# Prozess: ZNS-Import (Master Data Sync) + +**Status:** Draft / Konzept +**Datum:** 17.03.2026 + +## 1. Ausgangslage & Herausforderungen + +Das OEPS stellt die Stammdaten als ZIP-Datei (`zns.zip`) bereit, die in Form von textbasierten ASCII-Dateien (Codepage +850) vorliegen. Die Struktur ist starr, nicht relational und erfahrungsgemĂ€ĂŸ oft fehlerbehaftet oder unsauber +formatiert (Legacy-Spezifikation V2.4). + +ZusĂ€tzlich Ă€ndern sich LizenzstĂ€nde, Sperrlisten oder Registrierungen laufend. Weiters ist die Meldestelle oft +gezwungen, vor Ort manuelle Korrekturen vorzunehmen oder Daten aus anderen Quellen (z.B. ZuchtverbĂ€nde wie AWÖ) zu +integrieren. + +## 2. Architektonische Entscheidung: Event Sourcing & CQRS + +Um den Anforderungen (vollstĂ€ndige Historie, turnierspezifischer Datenstand, fehlertoleranter Import, **manuelle +Overrides**) gerecht zu werden, wird der ZNS-Import nach Prinzipien von **Event Sourcing** und **CQRS (Command Query +Responsibility Segregation)** konzipiert. + +* Wir ĂŒberschreiben keine Daten einfach (`UPDATE`), sondern hĂ€ngen Änderungen als Ereignisse (`EVENTS`) an. +* Dies ermöglicht es uns, den Stand einer Person oder eines Pferdes fĂŒr die Ewigkeit exakt zu rekonstruieren, selbst + wenn sich die Stammdaten Ă€ndern. + +### 2.1 Der Import-Ablauf (Die "Command" Seite) + +1. **Ingestion:** Der User (Meldestelle) lĂ€dt die `zns.zip` hoch oder triggert einen Import aus einer anderen Quelle ( + Zuchtverband). +2. **Parsing & Cleansing:** Ein dedizierter Importer-Service entpackt die ZIP, liest die Dateien zeilenweise (Codepage + 850!) und konvertiert die starren ASCII-Strings in nutzbare DTOs (Data Transfer Objects). Hier greifen erste + Reinigungs-Routinen. +3. **Event Generation:** Der Service vergleicht die geparsten Daten mit dem aktuellen Stand (der "Read Model" + Datenbank). + * Findet er einen neuen Akteur (Satznummer bisher unbekannt), erzeugt er ein `ActorCreatedEvent`. + * Findet er Änderungen (z.B. Lizenz wurde von R1 auf R2 erhöht, oder Sperre wurde gesetzt), erzeugt er ein + `ActorUpdatedEvent` (bzw. spezifischer `LicenseUpgradedEvent`, `ActorLockedEvent`). +4. **Manuelle Korrekturen (Overrides):** Wenn die Meldestelle vor Ort Daten korrigiert (weil die OEPS-Daten falsch + waren), erzeugt das System ein spezielles Event, z.B. `ManualActorCorrectionEvent`. Dieses Event hat eine **höhere + PrioritĂ€t** als zukĂŒnftige `ActorUpdatedEvents` aus dem ZNS-Import, solange der OEPS die Daten in seinem System nicht + korrigiert hat (Lösung z.B. ĂŒber einen "Ignorier-Zeitstempel" oder PrioritĂ€ts-Flags in der Projektion). +5. **Event Log:** Diese Events werden in einem zentralen Event Log (dem "Event Store") persistiert. Dies ist die + absolute Single Source of Truth. + +### 2.2 Die Datenbereitstellung (Die "Query" Seite) + +1. **Projection (Projektion):** Kleine "Listener" hören auf das Event Log und bauen daraus die relationale + Lesedatenbank (SQLite / PostgreSQL) auf. Hierbei wird die Logik angewandt, dass manuelle Korrekturen der Meldestelle + Vorrang vor veralteten Verbandsdaten haben. +2. **Turnier-Snapshot:** Wenn ein Turnier konfiguriert wird oder am Vortag aktualisiert wird, zieht sich das System + einen "Snapshot" (Schnappschuss) der aktuellen Stammdaten und verknĂŒpft diese mit der Turnier-ID. +3. **Zuchtverbands-Daten (Fremdformate):** Die Architektur erlaubt es uns leicht, neue Parser (z.B. fĂŒr AWÖ-Daten) zu + schreiben. Diese lesen die fremden Formate ein und generieren die gleichen Standard-Events (`HorseCreatedEvent`, aber + evtl. mit Lebensnummer statt OEPS-Satznummer), die dann problemlos in die bestehende Projektion einfließen. + +## 3. Datenhaltung (Konzeptuelles Modell) + +```mermaid +sequenceDiagram + participant User + participant ImporterService as ZNS / AWÖ Importer + participant EventStore as Event Log (Append Only) + participant Projections as DB (Read Models) + User ->> ImporterService: Upload zns.zip (Freitag vor Turnier) + ImporterService ->> ImporterService: Parse & Clean + + rect rgb(200, 220, 240) + Note right of ImporterService: Generierung von Import-Events + ImporterService ->> EventStore: Append: ActorUpdatedEvent(Satznummer: 123456, License: R2) + end + + User ->> EventStore: Manuelle Korrektur am Turniertag + rect rgb(240, 200, 200) + Note right of User: Generierung von Override-Events + User ->> EventStore: Append: ManualActorCorrectionEvent(Satznummer: 123456, Name: "Neuer Name") + end + + EventStore -->> Projections: Update Relational DB (z.B. aktueller Stand) + Note over Projections: Turniersystem liest nur
aus Read Models. Manuelle Korrekturen
gewinnen gegen Import-Daten. +``` + +## 4. Vorteile dieser Architektur + +* **Audit-Sicherheit:** Wir wissen exakt, *wann* sich *was* geĂ€ndert hat. Nichts geht verloren. +* **Archivierung:** Ein Turnier-Archiv muss nicht mehr mĂŒhsam als riesiger PDF/Daten-Dump gesichert werden. Wir können + das Turnier einfach anhand des Timestamps gegen den Event Store abfragen. +* **Fehlertoleranz:** Wenn ein Parsing-Fehler auftritt oder der OEPS kaputte Daten liefert, machen wir einfach ein + Rollback der fehlerhaften Events und projizieren die Datenbank neu. Wir zerschießen nicht die operativen Tabellen. +* **Erweiterbarkeit (Vision):** Wenn spĂ€ter ein "Ergebnis-Analyse-Service" oder ein "ZĂŒchter-Portal" angebunden wird, + können diese einfach die historischen Events abonnieren (Kafka/Message Queue). +* **Integration von Drittsystemen (ZuchtverbĂ€nde):** Fremddaten können durch eigene Parser in unsere Standard-Events + ĂŒbersetzt und nahtlos integriert werden. + +## 5. Zu klĂ€rende Details fĂŒr die Implementierung + +* **Sync-Mechanismus:** Wie kommen die Events vom Master-Server auf den Offline-Laptop im Plumpsklo? (Vermutlich eine + robuste Sync-Queue, Kafka könnte fĂŒr den Offline-Einsatz zu schwergewichtig sein, Alternativen evaluieren). +* **Event-Payload:** Definition der JSON-Struktur fĂŒr die wichtigsten Events (`zns.actor.updated`, + `zns.horse.registered`). +* **Merge-Logik:** Wie lange bleibt ein `ManualActorCorrectionEvent` gĂŒltig, bevor ein zukĂŒnftiges Update vom ZNS diesen + Wert wieder ĂŒberschreiben darf? diff --git a/docs/02_Domain/01_Core_Model/README.md b/docs/02_Domain/01_Core_Model/README.md new file mode 100644 index 00000000..f66d1a2a --- /dev/null +++ b/docs/02_Domain/01_Core_Model/README.md @@ -0,0 +1,19 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +--- +# Das Kern-Modell (Core Model) + +Dieses Verzeichnis ist die "Single Source of Truth" fĂŒr das destillierte, fachliche Wissen des Projekts. Nur was hier beschrieben ist, gilt als vereinbarte Wahrheit fĂŒr die Implementierung. + +## Struktur + +* `Entities/`: Beschreibt die zentralen fachlichen EntitĂ€ten des Systems (z.B. Event, Turnier, Akteur). +* `Processes/`: Dokumentiert die wichtigsten fachlichen Prozesse und AblĂ€ufe (z.B. Nennungsprozess, Ergebniserfassung). +* `Rules/`: Definiert explizite GeschĂ€ftsregeln und Validierungen. + +## Workflow + +Informationen in diesem Verzeichnis sind das Ergebnis der Analyse von externen Quellen (siehe `../02_Reference`) und Workshops (siehe `../03_Analysis`). +Jede Änderung am Core Model sollte nachvollziehbar und idealerweise durch ein ADR gestĂŒtzt sein. diff --git a/docs/02_Domain/01_Glossary/Ubiquitous_Language.md b/docs/02_Domain/01_Glossary/Ubiquitous_Language.md new file mode 100644 index 00000000..d95d5085 --- /dev/null +++ b/docs/02_Domain/01_Glossary/Ubiquitous_Language.md @@ -0,0 +1,245 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect & ÖTO/FEI Rulebook Expert +last_update: 2026-06-15 +sources: + - ÖTO 2026, Abschnitt A I, § 2 & § 3 & § 4 + - Domain Workshop 2026-03-17 + - Session 2026-03-24 (Architektur-Diskussion) + - Sanierung 2026-06-15 (Reality-Reset) +--- + +# Ubiquitous Language – Offizielle DomĂ€nen-Terminologie + +Dieses Dokument ist die **Single Source of Truth** fĂŒr alle Begriffe im Projekt. +Alle Begriffe sind exakt so zu verwenden – im Code, in der Dokumentation und in der Kommunikation. +Bei WidersprĂŒchen gilt: **ÖTO-Regelwerk → dieses Dokument → alle anderen Quellen**. + +--- + +## 1. Hierarchie der Veranstaltungs-Struktur + +``` +Veranstalter (OEPS-Mitgliedsverein) + └── Veranstaltung (interne ID, selbst vergeben) + ├── Typ: Turnier | Reitertreffen | SonderprĂŒfung | PS&S | Turnierartig + │ + ├── [wenn Typ = Turnier] + │ ├── Turniernummer (OEPS-vergeben, eindeutig) + │ ├── Turnierkategorie (C-NEU, C, B, A, ...) + │ ├── Sparte(n) (CDN, CSN, ...) + │ └── Bewerb / PrĂŒfung (Bewerbsnummer, fortlaufend) + │ └── Abteilung (mindestens 1) + │ ├── Teilnehmerkreis (Lizenz, Pferdealter, ...) + │ └── Eigene Platzierung / Siegerehrung + │ + ├── [wenn Typ = Reitertreffen] + │ └── Bewerbe (ohne offizielle Turniernummer) + │ + └── [Cup / Serie / Meisterschaft → Querverweis auf mehrere Veranstaltungen] + └── eigenes Reglement (siehe Abschnitt 4) +``` + +--- + +## 2. Kern-Begriffe (Alphabetisch) + +### A + +| Begriff | Definition | ÖTO-Referenz | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| **Abteilung** | **Kleinste ausfĂŒhrbare, atomare Einheit** fĂŒr Nennungen, Startlisten, Ergebnisse und Auswertungen. Untereinheit eines Bewerbs mit eigenem Teilnehmerkreis (Lizenz, Pferdealter etc.). ErhĂ€lt eine fortlaufende **Abteilungsnummer** (1, 2, ...) innerhalb des Bewerbs. Referenz auf Startliste/Ergebnisliste: `BW: 9 Abt: 1` bzw. `9-1`. | | +| **Abteilungs-Typen:** `SEPARATE_SIEGEREHRUNG` (= eigene Platzierung, eigene Siegerehrung, separater Ergebnislauf) | `ORGANISATORISCH` (= organisatorische Teilung, z.B. zur Ablaufoptimierung; Platzierung/Preise werden gemeinsam mit anderen Abteilungen dieses Bewerbs gefĂŒhrt). | | +| Die ÖTO definiert sparten- und klassenabhĂ€ngige Schwellenwerte, ab wievielen Startern eine Abteilung **verpflichtend** getrennt werden muss. Bei Überschreitung gibt das System eine **WARNUNG** (kein harter Fehler) – der TBA hat das letzte Wort (→ *Override-Event*). VollstĂ€ndige Schwellenwert-Tabellen: → [`Abteilungs-Trennungs-Schwellenwerte.md`](../02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md) | ÖTO § 2 Abs. 7, § 39 | | +| **Akteur** | Historischer Begriff (siehe → *Stammdaten*). Oberbegriff fĂŒr alle Personen (Reiter, Richter, FunktionĂ€re, Besitzer) und Organisationen (Vereine), die im System interagieren. | – | +| **Ausschreibung** | Das offizielle Dokument, das alle Bedingungen eines Turniers festlegt. Pflichtfelder gemĂ€ĂŸ ÖTO (A-Satz der ZNS-Schnittstelle). | ÖTO Ausschreibungs-Struktur | +| **Austragungsplatz** | Physischer Ort (Platz, Arena, Halle) innerhalb einer Veranstaltung, auf dem Bewerbe stattfinden. Typ: `"Austragungsplatz"` (Wettkampf) oder `"Vorbereitungsplatz"` (Einreiten). Kann einer Sparte zugeordnet sein. Im Code als EntitĂ€t **`Austragungsplatz`** (events-domain) abgebildet. | – | + +### B + +| Begriff | Definition | ÖTO-Referenz | +|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| +| **Bewerb** | Synonym: „PrĂŒfung". Die einzelne sportliche PrĂŒfung innerhalb eines Turniers (z.B. „PrĂŒfung Nr. 9: StandardspringprĂŒfung 95 cm"). ErhĂ€lt eine fortlaufende **Bewerbsnummer** (01, 02, ...). Ein Bewerb besteht aus **mindestens einer Abteilung**. Die → *Abteilung* ist die kleinste Einheit fĂŒr Nennungen, Startlisten und Ergebnisse. | ÖTO § 2 Abs. 7 | + +### C + +| Begriff | Definition | ÖTO-Referenz | +|---------|------------------|----------------| +| **Cup** | Siehe → *Serie*. | ÖTO § 2 Abs. 8 | + +### F + +| Begriff | Definition | ÖTO-Referenz | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| +| **FEI-ID** | Eindeutige Identifikationsnummer der Internationalen Reiterlichen Vereinigung (FEI) fĂŒr Reiter und Pferde. | FEI General Regulations | +| **FunktionĂ€r** | Person mit einer definierten Rolle bei einem Turnier (Richter, Parcoursbauer, TBA, ...). Qualifikation wird gegen `RICHT01.DAT` geprĂŒft. Im Code als EntitĂ€t **`Funktionaer`** abgebildet. | ÖTO FunktionĂ€rs-Qualifikation | + +### G + +| Begriff | Definition | ÖTO-Referenz | +|-----------------------|------------------------------------------------------------------------------------------------------------------|---------------------------| +| **Gastreiter** | Reiter mit auslĂ€ndischer StaatsbĂŒrgerschaft, der nicht fĂŒr einen österreichischen Verein startet. | ÖTO Teilnahmeberechtigung | +| **GebĂŒhren-Verzicht** | Der Veranstalter kann die NachnenngebĂŒhr fĂŒr einzelne Nennungen erlassen. Wird als explizites Event gespeichert. | ÖTO GebĂŒhrenstruktur | + +### K + +| Begriff | Definition | ÖTO-Referenz | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| **Kategorie** | Das Niveau eines Turniers und/oder seiner Bewerbe bzw. die Teilnahmeberechtigung daran. Nationale Kategorien: `C-NEU`, `C`, `B*`, `B`, `A`, `A*`. | ÖTO § 3 Abs. 4 | +| **Bewerbsklasse** | FrĂŒher „Turnierklasse“. Schwierigkeitsgrad eines Bewerbs. Springen: E0 (60–90 cm), A (105–110 cm), L, M, S. Dressur: E, A, L, M, S (nach Aufgabe). Stammdaten in `bewerbs_klassen`. | ÖTO B-Teil | +| **Kombination** | Zwei oder mehr Turniere (ggf. unterschiedlicher Sparten) die am selben Ort/Datum stattfinden. Jedes Turnier behĂ€lt seine eigene Turniernummer. Genehmigung durch LFV/OEPS erforderlich. | ÖTO § 4 | +| **Kopfnummer** | *National (OEPS):* 4-stellige Registrierungsnummer eines Pferdes beim OEPS. **Nicht als eindeutige ID geeignet** – kann sich Ă€ndern. Dient zur schnellen Suche/Eingabe in der Meldestelle (Autocomplete), aber nicht als DatenbankschlĂŒssel. *Turnier:* TemporĂ€re Startnummer fĂŒr das spezifische Turnier (ebenfalls nicht persistent). | – | +| **Konto** | Kontobasierte Abrechnung pro Zahler (nicht nur pro Reiter). Basis fĂŒr das „Hansi-Szenario" (Guthaben bei Transfer). | Billing Context | + +### L + +| Begriff | Definition | ÖTO-Referenz | +|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| **Lebensnummer** | 9-stellige (national) bzw. 15-stellige (international, UELN) Nummer, die ein Pferd bei der Geburt vom Zuchtverband erhĂ€lt. Bei auslĂ€ndischen Pferden im OEPS oft **generiert** → **nicht zur Suche geeignet**. Die ZNS-Daten zu Lebensnummern sind erfahrungsgemĂ€ĂŸ inkonsistent und widersprĂŒchlich (z.B. Farbe `"Braun"` vs. `"Brauner"` fĂŒr dasselbe Pferd). PrimĂ€rer SchlĂŒssel fĂŒr den Datenaustausch bleibt die → *Satznummer*. | – | +| **Lizenz (Reiterlizenz)** | Qualifikationsstufe eines Reiters (z.B. `R1`, `RD1`, `RD2`, `RS2`). Ein Reiter hat 0..1 Reiterlizenz. Stammdaten in `reiterlizenzen`, Referenz am Reiter `reit_lizenz_id`. | ÖTO Teilnahmeberechtigung | +| **Fahr-Lizenz** | Lizenzkategorie fĂŒr den Fahrsport (z.B. `F1`, `F2`). Ein Reiter hat 0..1 Fahr-Lizenz. Stammdaten in `fahr_lizenzen`, Referenz am Reiter `fahr_lizenz_id`. | ÖTO Teilnahmeberechtigung | +| **Startkarte** | Nachweis der JahresgebĂŒhr. Ein Reiter hat 0..1 Startkarte. Stammdaten in `startkarten`, Referenz am Reiter `startkarte_id`. | ÖTO Teilnahmeberechtigung | + +### M + +| Begriff | Definition | ÖTO-Referenz | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| **Meisterschaft** | Übergeordneter Wettbewerb, der Ergebnisse aus Bewerben bei **mindestens zwei Turnieren** aggregiert und zu einem Endklassement fĂŒhrt. Darf nur auf Turnieren der Kategorie A* und A veranstaltet werden (außer Sonderregelungen der Sparte). Besitzt ein **eigenes Reglement** (siehe Abschnitt 4). | ÖTO § 2 Abs. 8, § 3 Abs. 4 | + +### N + +| Begriff | Definition | ÖTO-Referenz | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| +| **Nennung** | Die verbindliche Anmeldung eines Paares (Reiter & Pferd) zu einem Bewerb. | Registration Context | +| **Nennschluss** | Frist, bis zu der Nennungen ohne NachnenngebĂŒhr eingereicht werden können. | ÖTO Nennschluss | +| **Nennungs-Transfer** | Tausch von Reiter und/oder Pferd innerhalb einer bestehenden Nennung. **Kein Storno + Neu**, sondern eine Transfer-Operation. Bereits bezahltes Nenngeld wird als Guthaben gefĂŒhrt. | Registration Context | +| **NachnenngebĂŒhr** | ZusatzgebĂŒhr fĂŒr Nennungen nach dem Nennschluss. Kann vom Veranstalter erlassen werden (→ *GebĂŒhren-Verzicht*). | ÖTO GebĂŒhrenstruktur | + +### O + +| Begriff | Definition | ÖTO-Referenz | +|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| **OEPS** | Österreichischer Pferdesportverband. Übergeordnete Organisation, vergibt Turniernummern und fĂŒhrt das ZNS. | – | +| **Override-Event** | Explizit gespeichertes Ereignis, wenn der TBA oder die Meldestelle eine Regelwerk-Warnung bewusst ĂŒberschreibt. Das System gibt **niemals** einen harten Fehler bei Regelkonflikten – immer nur eine Warnung + Override-Möglichkeit. | Domain Workshop 2026-03-17 | + +### P + +| Begriff | Definition | ÖTO-Referenz | +|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| +| **Pferdesportliche Veranstaltung** | Oberbegriff laut ÖTO fĂŒr alle Arten von Veranstaltungen: Turniere, Reitertreffen, SonderprĂŒfungen, PS&S, Turnierartige Veranstaltungen. In unserer Software entspricht dies dem Begriff → *Veranstaltung*. | ÖTO § 2 Abs. 1 | +| **PrĂŒfung** | Synonym fĂŒr → *Bewerb*. In der ÖTO-Ausschreibung wird der Begriff „PrĂŒfung" verwendet. | ÖTO § 2 Abs. 7 | + +### R + +| Begriff | Definition | ÖTO-Referenz | +|--------------------|------------------------------------------------------------------------------------------------------------------------------|--------------| +| **RichterEinsatz** | Value Object, das den Einsatz eines FunktionĂ€rs (Richter, Aufsicht) in einem Bewerb beschreibt. Felder: `funktionaerId` (Referenz auf FunktionĂ€r) und `position` (z.B. `"C"`, `"M"`, `"B"`, `"Aufsicht"`). Im Code als **`RichterEinsatz`** (entries-domain) abgebildet. | ÖTO FunktionĂ€rs-Qualifikation | +| **Richtverfahren** | Das Bewertungsverfahren eines Bewerbs (z.B. § 204/4 Stilspringen, § 218 Einlauf). Bestimmt, wie Ergebnisse berechnet werden. | ÖTO B-Teil | + +### S + +| Begriff | Definition | ÖTO-Referenz | +|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| **Reiter** | Eine Person, die an einem Bewerb teilnimmt. Spezialisierung einer Person mit pferdesportlichen Attributen (Lizenz, Startkarte). Im Code als EntitĂ€t **`Reiter`** abgebildet. Datenquelle primĂ€r `LIZENZ01.DAT`. | ÖTO § 2 Abs. 11 | +| **Satznummer** | *Pferd:* 10-stellige, rein numerische ID (`0000123456`). **PrimĂ€rer SchlĂŒssel fĂŒr den Datenaustausch.** *Reiter:* 6-stellige, rein numerische ID. | ZNS-Schnittstelle | +| **Serie** | Synonym fĂŒr → *Cup*. Übergeordneter Wettbewerb, der Ergebnisse aus Bewerben bei **mindestens zwei Turnieren** aggregiert. Besitzt ein **eigenes Reglement** (siehe Abschnitt 4). | ÖTO § 2 Abs. 8 | +| **Sparte** | Die unterschiedlichen Arten von Turnieren oder Bewerben (z.B. Dressur = CDN, Springen = CSN). Bestimmt das anzuwendende Richtverfahren und das Regelwerk. | ÖTO § 2 Abs. 9, § 3 Abs. 2 | +| **Sperrliste** | Vom Verband gefĂŒhrte Liste von Personen oder Pferden, die aktuell nicht startberechtigt sind (meist wegen offener Zahlungen). | – | +| **Sportförderbeitrag** | GebĂŒhr, die **pro Start** anfĂ€llt (nicht pro Nennung!). Relevant bei Mehrfach-Starts. | ÖTO GebĂŒhrenordnung | +| **Stammdaten** | Die zentrale Library of Truth (`masterdata-context`) fĂŒr alle statischen Informationen: Personen, Pferde, Vereine, sowie das ÖTO-Regelwerk (Richtverfahren, Paragraphen, GebĂŒhrensĂ€tze). | – | +| **Startkarte** | Nachweis, dass die JahresgebĂŒhr fĂŒr die Lizenz bezahlt wurde. Ohne aktive Startkarte ist national kein Start möglich. | ÖTO Teilnahmeberechtigung | +| **Startwunsch** | PrĂ€ferenz eines Reiters bezĂŒglich seiner Position in der Startliste (vorne/hinten). | Registration Context | + +### T + +| Begriff | Definition | ÖTO-Referenz | +|-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| **TBA** | Turnierbeauftragter. Hat bei Regelkonflikten immer das letzte Wort. Jede Überschreibung wird als → *Override-Event* gespeichert. | ÖTO § 24/§ 25 | +| **TurnierArtikel** | Abrechenbare Leistung oder Produkt im Rahmen einer Veranstaltung (z.B. Ansage, Heu, Startgeld). Preis in Cent (Long). Typ: `"AUTOMATISCH"` (systemseitig erzeugt) oder `"MANUELL"` (manuell erfasst). Im Code als EntitĂ€t **`TurnierArtikel`** (events-domain, Billing Context) abgebildet. | Billing Context | +| **Tierwohl-Euro** | GebĂŒhr, die **pro Start** anfĂ€llt (nicht pro Nennung!). | ÖTO GebĂŒhrenordnung | +| **Turnier** | In unserer Software: Eine pferdesportliche Veranstaltung mit einer offiziellen **Ausschreibung** und einer vom OEPS/LFV vergebenen, eindeutigen **Turniernummer**. Entspricht ÖTO § 2 Abs. 2. Ist eine Spezialisierung von → *Veranstaltung*. | ÖTO § 2 Abs. 2, § 5, § 24 | +| **Turniernummer** | Offizielle, vom OEPS vergebene **5-stellige** Kennung eines Turniers (z.B. `26128`). Sie ist eindeutig und Voraussetzung fĂŒr die offizielle Ausschreibung. | ÖTO § 3, ZNS A-Satz | +| **Teilnehmerkreis-EinschrĂ€nkung** | Optionale EinschrĂ€nkung des zulĂ€ssigen Teilnehmerkreises eines Turniers gemĂ€ĂŸ ÖTO-Gliederung (§ 3), dargestellt ĂŒber Zusatz-Buchstaben (z.B. `-H`, `-P`, `-J`). | ÖTO § 3 | +| **Turnierkategorie** | Siehe → *Kategorie*. | ÖTO § 3 Abs. 4 | +| **Turnierkassa** | Kassa auf Ebene des einzelnen → *Turniers*. HĂ€lt Belege, TagesabschlĂŒsse und BarbestĂ€nde pro Turnier. Wird in der → *Veranstaltungs‑Kassa* konsolidiert. | Billing Context | +| **TeilnehmerKonto** | Konto eines Zahlers auf Ebene der → *Veranstaltung* (nicht nur eines Turniers). Aggregiert Saldo, Einzahlungen und Verrechnungen ĂŒber alle → *Turniere* derselben Veranstaltung hinweg (Multi‑Turnier‑Aggregation). Ermöglicht, dass eine einzelne Zahlung mehrere Rechnungen in verschiedenen Turnieren derselben Veranstaltung ausgleicht. | Billing Context | + +### V + +| Begriff | Definition | ÖTO-Referenz | +|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| **Veranstaltung** | In unserer Software: Der Oberbegriff fĂŒr jede Art von pferdesportlicher Veranstaltung, die von einem Verein durchgefĂŒhrt wird. ErhĂ€lt eine **intern vergebene ID**. Entspricht dem ÖTO-Oberbegriff „Pferdesportliche Veranstaltung" (§ 2 Abs. 1). Kann vom Typ Turnier, Reitertreffen, SonderprĂŒfung, PS&S oder Turnierartig sein. | ÖTO § 2 Abs. 1 | +| **Veranstalter** | OEPS-Mitgliedsverein (ĂŒber LFV angeschlossen), der eine Veranstaltung ausrichtet. Besitzt eine **Vereinsnummer**. Im Code als EntitĂ€t **`Verein`** abgebildet. | ÖTO § 2 Abs. 12 | +| **Veranstaltungs‑Kassa** | Kassen- und AbrechnungsfĂŒhrer auf Ebene der → *Veranstaltung*. Konsolidiert alle Zahlungen, Belege und RĂŒckgelder ĂŒber mehrere → *Turniere* derselben Veranstaltung. Dient als zentrale Sammelkasse; kann ZahlvorgĂ€nge turnierĂŒbergreifend splitten und konsolidieren. | Billing Context | + +### Z + +| Begriff | Definition | ÖTO-Referenz | +|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------| +| **Zahlvorgang** | Eine einzelne Zahlungstransaktion (Bar, Karte, Überweisung), die einen Betrag einem → *TeilnehmerKonto* gutschreibt und dabei **eine Zahlung auf mehrere Rechnungen/Belege** aufteilen kann – auch turnierĂŒbergreifend innerhalb derselben → *Veranstaltung*. | Billing Context | +| **ZNS** | Zentrales Nennsystem des OEPS. Datenaustausch-Format fĂŒr Stammdaten (Reiter, Pferde) und Nennungen. Quelle der Wahrheit fĂŒr Akteurs-Daten. | ZNS-Schnittstelle | + +--- + +## 3. Bounded Contexts & Zuordnung + +| Begriff | PrimĂ€rer Bounded Context | +|-------------------------------------------------------------------|-------------------------------| +| Veranstaltung, Turnier, Ausschreibung, Veranstalter | `event-management-context` | +| Bewerb, Abteilung, Startliste, Ergebnis, Richtverfahren | `competition-context` | +| Nennung, Nennungs-Transfer, Startwunsch, ZNS-Import | `registration-context` | +| Reiter, Pferd, Lizenz, FunktionĂ€r, Kopfnummer, Satznummer, Verein | `masterdata-context` | +| Nenngeld, Startgeld, Konto, Transaktion, Sportförderbeitrag | `billing-context` | +| Cup, Serie, Meisterschaft, Reglement, Endklassement | `series-context` *(Phase 2+)* | +| Login, Rolle, Berechtigung | `identity-context` | + +--- + +## 4. Meisterschaften, Cups & Serien – Eigene Reglements + +> ⚠ **Wichtiger Hinweis fĂŒr zukĂŒnftige Entwicklungsphasen** + +Jede Meisterschaft, jeder Cup und jede Serie besitzt ein **eigenes, individuelles Reglement**. +Dieses Reglement ist **nicht** durch die ÖTO allein abgedeckt, sondern wird vom jeweiligen +Veranstalter (LFV, OEPS-Referat, Verein) separat erlassen. + +Ein Reglement definiert typischerweise: + +| Abschnitt | Inhalt | Relevanter Context | +|---------------------------------------|-------------------------------------------------------------------------------------------------------------|----------------------------------------------| +| **PrĂ€ambel & Geltungsbereich** | Name, TrĂ€ger, Verweis auf ÖTO + Sonderbestimmungen des LFV | `series-context` | +| **Teilnahmeberechtigung** | Lizenzklasse, Altersklasse, Vereinsmitgliedschaft, Pferdealter, Paar-Bindung (Reiter+Pferd oder nur Reiter) | `registration-context` (Filterlogik) | +| **Qualifikation & WertungsprĂŒfungen** | Welche Turniere/Bewerbe zĂ€hlen, Pflichtteilnahme vs. Mindestanzahl | `event-management-context` | +| **Punktesystem & Berechnungsmodus** | Addition der Ergebnisse, Fixpunkte nach Platzierung, Faktoren (Finale 1,5-fach), Streichresultate | `competition-context` (Ergebnis-Aggregation) | +| **Ex-aequo-Regelung** | Tiebreaker-Regeln bei Punktgleichheit | `competition-context` | +| **Das Finale** | Teilnahmevoraussetzung (z.B. Top 15), Startreihenfolge (umgekehrter Zwischenstand) | `competition-context` | +| **Preise & Ehrungen** | Titel, Ehrengaben, Preisgeld-VerteilungsschlĂŒssel | `billing-context` | +| **Proteste & SonderfĂ€lle** | Einspruchsfristen, Regelung bei Turnierabsagen / Höherer Gewalt | `series-context` | + +**Konsequenz fĂŒr die Architektur:** + +- Der `series-context` muss das Reglement als **konfigurierbare EntitĂ€t** abbilden (kein Hard-Coding). +- Verschiedene Cups/Serien können **unterschiedliche Punktesysteme** haben → das Berechnungsmodell muss pluggable sein. +- Die **Paar-Bindung** (Punkte an Reiter+Pferd vs. nur Reiter) ist eine kritische Designentscheidung pro Reglement. +- Referenz-Dokument: [ + `docs/02_Domain/02_Reference/OETO_Regelwerk/Struktur-Meisterschafts-Cup-Reglements_OETO.md`](../02_Reference/OETO_Regelwerk/Struktur-Meisterschafts-Cup-Reglements_OETO.md) + +--- + +## 5. MVP-Scope (Phase 1) – In Scope / Out of Scope + +| Begriff / Feature | Phase 1 | BegrĂŒndung | +|----------------------------------------|-------------|----------------------------------------------------| +| Turnier (Typ = Turnier) | ✅ In Scope | Kern des Systems | +| Kategorie C und C-NEU | ✅ In Scope | MVP-Entscheidung | +| Sparten CDN (Dressur) + CSN (Springen) | ✅ In Scope | MVP-Entscheidung | +| Reitertreffen, SonderprĂŒfung, PS&S | 🔜 Phase 2 | Architektur lĂ€sst es zu | +| Cup / Serie / Meisterschaft | 🔜 Phase 2+ | Eigenes Reglement erforderlich (siehe Abschnitt 4) | +| Kombination von Turnieren (§ 4) | 🔜 Phase 2 | Datenmodell ist vorbereitet | +| Kategorie A, B (Meisterschaften) | 🔜 Phase 2+ | Nur auf A*/A erlaubt | + +--- + +*Erstellt: 2026-03-24 | Autoren: Lead Architect, ÖTO/FEI Rulebook Expert, Curator* +*Basiert auf: ÖTO 2026 § 2, § 3, § 4 | Domain Workshop 2026-03-17 | Session 2026-03-24 | Update: 2026-06-15 (Reality-Reset & Alignment)* diff --git a/docs/02_Domain/03_Analysis/Database_Schema_Draft.sql b/docs/02_Domain/03_Analysis/Database_Schema_Draft.sql new file mode 100644 index 00000000..a3e2e4a0 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Database_Schema_Draft.sql @@ -0,0 +1,241 @@ +-- Database Schema Draft for Meldestelle (Offline-First) +-- Dialect: SQLite (compatible with SQLDelight) +-- Status: Draft / Proposal +-- Based on: OEPS Legacy Spec V2.4 & Domain Analysis + +-- ================================================================== +-- 1. CORE INFRASTRUCTURE (Sync & Audit) +-- ================================================================== +-- Every table should ideally have these fields, but for brevity +-- they are implied or added where critical. +-- id: TEXT NOT NULL PRIMARY KEY (UUID) +-- created_at: INTEGER NOT NULL (Epoch Millis) +-- updated_at: INTEGER NOT NULL (Epoch Millis) +-- version: INTEGER NOT NULL (Optimistic Locking / Sync Counter) +-- is_deleted: INTEGER NOT NULL DEFAULT 0 (Soft Delete) + +-- ================================================================== +-- 2. MASTER DATA (Stammdaten) +-- ================================================================== + +-- Akteure: Personen und Organisationen +-- Covers: Reiter, Richter, Besitzer, Vereine +CREATE TABLE actor ( + id TEXT NOT NULL PRIMARY KEY, + type TEXT NOT NULL, -- 'PERSON', 'ORGANIZATION' + + -- Display Data + first_name TEXT, -- NULL for Organizations + last_name TEXT NOT NULL, -- Name or Org-Name + + -- OEPS Specifics (Legacy Spec) + oeps_id TEXT, -- 'Satznummer' (6 digits for Person, 4 for Club) + oeps_category TEXT, -- 'Verein', 'Reiter', 'Richter' + + -- Licenses & Status + license_code TEXT, -- e.g. 'R1', 'RD3' + has_start_card INTEGER NOT NULL DEFAULT 0, -- Boolean: Paid annual fee? + is_locked INTEGER NOT NULL DEFAULT 0, -- Boolean: Sperrliste? + + -- Contact & Meta + nationality TEXT NOT NULL DEFAULT 'AUT', -- ISO 3-Letter + contact_json TEXT, -- Address, Phone, Email + + -- Sync Meta + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1 +); + +CREATE INDEX idx_actor_oeps_id ON actor(oeps_id); +CREATE INDEX idx_actor_name ON actor(last_name, first_name); + + +-- Pferde +CREATE TABLE horse ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + + -- Identification + oeps_id TEXT, -- 'Satznummer' (10 digits) - CRITICAL for Export + head_number_permanent TEXT, -- 'Kopfnummer' (e.g. A123) + life_number TEXT, -- 'Lebensnummer' (Zucht) + fei_id TEXT, + + -- Details + birth_year INTEGER, + gender TEXT, -- 'M', 'W', 'G' (Gelding/Wallach) + color TEXT, + sire_name TEXT, -- Vater (Denormalized for search) + dam_name TEXT, -- Mutter + + -- Owner Link + owner_id TEXT, -- FK to actor.id + + -- Status + is_locked INTEGER NOT NULL DEFAULT 0, -- Sperrliste + + -- Sync Meta + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1 +); + +CREATE INDEX idx_horse_oeps_id ON horse(oeps_id); +CREATE INDEX idx_horse_head_num ON horse(head_number_permanent); +CREATE INDEX idx_horse_name ON horse(name); + +-- ================================================================== +-- 3. EVENT STRUCTURE +-- ================================================================== + +CREATE TABLE event ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + start_date INTEGER NOT NULL, -- Epoch Day + end_date INTEGER NOT NULL, + location TEXT, + organizer_id TEXT NOT NULL, -- FK to actor.id + + status TEXT NOT NULL DEFAULT 'PLANNING' -- PLANNING, ACTIVE, ARCHIVED +); + +CREATE TABLE tournament ( + id TEXT NOT NULL PRIMARY KEY, + event_id TEXT NOT NULL REFERENCES event(id), + + -- OEPS Spec + oeps_number TEXT NOT NULL, -- 5 digits (e.g. 21001) + category TEXT, -- e.g. 'CSN-A' + ruleset TEXT NOT NULL DEFAULT 'OETO', -- 'OETO', 'FEI' + + -- Sync Meta + updated_at INTEGER NOT NULL +); + +-- Bewerbe (Competitions) +-- Note: If a competition is split into 2 departments (Abteilungen), +-- we create 2 rows here to match the OEPS 'B-Satz' logic. +CREATE TABLE competition ( + id TEXT NOT NULL PRIMARY KEY, + tournament_id TEXT NOT NULL REFERENCES tournament(id), + + -- Identification + code_internal TEXT NOT NULL, -- '01', '02' (2 digits) + code_official TEXT, -- '001' (3 digits, optional) + division_id INTEGER NOT NULL DEFAULT 0, -- 'Abteilung' (0=None, 1=1st, 2=2nd) + + -- Description + title TEXT NOT NULL, + category TEXT, -- e.g. 'LM', 'S*' + discipline TEXT NOT NULL, -- 'D', 'S', 'C' (Dressage, Jumping, Combined) + + -- Rules & Scoring + scoring_method TEXT NOT NULL, -- 'A0', 'C', 'DRESSAGE_PERCENT' + start_fee INTEGER NOT NULL DEFAULT 0, -- In Cents + + -- State + status TEXT NOT NULL DEFAULT 'OPEN', -- OPEN, CLOSED_FOR_ENTRIES, RUNNING, FINISHED, SIGNED_OFF + + -- Sync Meta + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_comp_tournament ON competition(tournament_id); + +-- ================================================================== +-- 4. SPORT & PROCESS +-- ================================================================== + +-- Nennungen (Entries) +-- Represents the intent to start. +CREATE TABLE entry ( + id TEXT NOT NULL PRIMARY KEY, + competition_id TEXT NOT NULL REFERENCES competition(id), + + -- The Pair + horse_id TEXT NOT NULL REFERENCES horse(id), + rider_id TEXT NOT NULL REFERENCES actor(id), + + -- Financials + responsible_person_id TEXT REFERENCES actor(id), -- Who pays? + fee_agreed INTEGER NOT NULL, -- In Cents (Snapshot of price at entry time) + payment_status TEXT NOT NULL DEFAULT 'PENDING', -- PENDING, PAID, WAIVED + + -- Validation Override (The "Human Factor") + validation_status TEXT NOT NULL DEFAULT 'OK', -- OK, WARNING, BLOCKED + override_comment TEXT, -- Why was this allowed despite warning? + + -- Sync Meta + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_entry_comp ON entry(competition_id); +CREATE INDEX idx_entry_rider ON entry(rider_id); + +-- Startliste (Start Order) +-- Subset of entries that actually start. +CREATE TABLE start_list_entry ( + id TEXT NOT NULL PRIMARY KEY, + entry_id TEXT NOT NULL REFERENCES entry(id), + + -- Ordering + start_order INTEGER, -- 1, 2, 3... + start_time_planned INTEGER, -- Epoch Millis (optional) + + -- Tournament Specifics + head_number_event TEXT, -- Startnummer am Turnier (kann von A123 abweichen) + + -- Status + status TEXT NOT NULL DEFAULT 'READY', -- READY, STARTED, DNS (Did Not Start) + + UNIQUE(entry_id) +); + +-- Ergebnisse (Results) +CREATE TABLE result ( + id TEXT NOT NULL PRIMARY KEY, + start_list_entry_id TEXT NOT NULL REFERENCES start_list_entry(id), + + -- The Outcome + rank INTEGER, -- 1, 2, 3... (NULL if eliminated) + + -- Scoring Details (Polymorphic based on Competition Type) + points_jump_faults DECIMAL(5,2), -- Springfehler + time_taken_ms INTEGER, -- Zeit in Millisekunden + score_dressage_percent DECIMAL(5,3), -- 72.500 + score_dressage_total DECIMAL(6,2), -- Summe Punkte + + -- Status Flags + classification TEXT NOT NULL DEFAULT 'OK', -- OK, EL (Elim), RET (Retired), DIS (Disq) + + -- Detailed Marks (JSON) + -- e.g. { "judge_C": 7.5, "judge_H": 7.2, "obstacles": [...] } + details_json TEXT, + + -- Money + prize_money INTEGER DEFAULT 0, -- In Cents + + -- Audit + updated_by_user_id TEXT, + updated_at INTEGER NOT NULL +); + +CREATE INDEX idx_result_starter ON result(start_list_entry_id); + +-- ================================================================== +-- 5. AUDIT LOG (NFR-07) +-- ================================================================== + +CREATE TABLE audit_log ( + id TEXT NOT NULL PRIMARY KEY, + entity_type TEXT NOT NULL, -- 'RESULT', 'ENTRY' + entity_id TEXT NOT NULL, + action TEXT NOT NULL, -- 'CREATE', 'UPDATE', 'DELETE' + + user_id TEXT, + timestamp INTEGER NOT NULL, + + changes_json TEXT -- { "score_old": 7.0, "score_new": 7.5 } +); diff --git a/docs/02_Domain/03_Analysis/Domain_Workshop_Agenda.md b/docs/02_Domain/03_Analysis/Domain_Workshop_Agenda.md new file mode 100644 index 00000000..8a9cc367 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Domain_Workshop_Agenda.md @@ -0,0 +1,36 @@ +# ToDo-Liste: Domain Workshop (Kick-off) + +**Datum:** Heute Abend +**Teilnehmer:** Owner (Fachexperte), đŸ—ïž [Lead Architect], 📜 [ÖTO/FEI Rulebook Expert] + +## Ziel des Abends + +Start der fachlichen KlĂ€rung (Phase 3). Definition des ersten Kernprozesses, um die Basis fĂŒr die Feature-Entwicklung zu +schaffen. + +## Agenda & ToDos + +### 1. Auswahl des Einstiegs-Themas + +*Entscheidung treffen, mit welchem Bounded Context wir starten:* + +- [ ] **Option A: Stammdaten & Akteure** + *Was definiert ein Pferd, einen Reiter, einen Verein? (Fokus auf OEPS-IDs, Lizenzen, Startkarten).* +- [ ] **Option B: Turnier- & Bewerbsstruktur** + *Wie ist ein Turnier aufgebaut? (Kategorien, Bewerbe, Abteilungen, Richtverfahren).* +- [ ] **Option C: Der Nennungsprozess** + *Der Kern-Workflow: Wie meldet ein Reiter ein Pferd an? Welche Validierungen (Sperrlisten, Lizenzen) greifen hier?* + +### 2. DurchfĂŒhrung des Workshops (am gewĂ€hlten Thema) + +- [ ] **Prozess-Skizzierung:** Der Owner beschreibt den fachlichen Ablauf aus der Praxis. +- [ ] **Regel-Check:** Der 📜 *[ÖTO/FEI Rulebook Expert]* prĂŒft den skizzierten Ablauf live gegen das hinterlegte + ÖTO-2026-Regelwerk und weist auf Edge-Cases hin. +- [ ] **Modellierung:** Der đŸ—ïž *[Lead Architect]* leitet daraus die ersten EntwĂŒrfe fĂŒr Aggregates und EntitĂ€ten ab. + +### 3. Abschluss & Übergabe + +- [ ] **Festhalten der Ergebnisse:** Dokumentation der getroffenen Definitionen im Ordner + `docs/03_Domain/01_Core_Model/`. +- [ ] **Übergabe an QA & UX:** Auftrag an den *QA Specialist* (fĂŒr Gherkin-Szenarien) und den *UI/UX Designer* (fĂŒr + Wireframes) basierend auf den Ergebnissen des Abends erteilen. diff --git a/docs/02_Domain/03_Analysis/Domain_Workshop_Results_2026-03-17.md b/docs/02_Domain/03_Analysis/Domain_Workshop_Results_2026-03-17.md new file mode 100644 index 00000000..e02664c2 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Domain_Workshop_Results_2026-03-17.md @@ -0,0 +1,102 @@ +# Ergebnisse Domain Workshop (17.03.2026) + +**Teilnehmer:** Owner, đŸ—ïž [Lead Architect], 📜 [ÖTO/FEI Rulebook Expert], đŸ‘· [Backend Developer], 🎹 [Frontend Expert], +đŸ–Œïž [UI/UX Designer] + +## 📜 [ÖTO/FEI Rulebook Expert] - Erkenntnisse + +1. **AutoritĂ€t des Turnierbeauftragten (TBA):** + Die Analyse der ÖTO (§ 24, § 25) bestĂ€tigt die Aussage des Owners: Der TBA ist die höchste Instanz vor Ort und kann + in Absprache mit dem Veranstalter von der Ausschreibung abweichende, fĂŒr alle verbindliche Entscheidungen treffen. + Das System muss daher bei Regelkonflikten immer einen manuellen "Override" durch die Meldestelle ermöglichen. + +2. **GebĂŒhrenlogik & Abrechnung (Das "Hansi"-Szenario):** + Die KomplexitĂ€t der GebĂŒhrenstruktur wurde stark verdeutlicht. Es muss klar unterschieden werden zwischen: + * **Nenngeld** (GrundgebĂŒhr, oft im Voraus via ZNS bezahlt) + * **Startgeld** (pro Bewerb) + * **NachnenngebĂŒhren** (Aufschlag bei spĂ€ter Nennung, oft verhandelbar) + * **NebengebĂŒhren** (Sportförderbeitrag, Tierwohl-Euro - pro Start fĂ€llig) + Die Möglichkeit fĂŒr den Veranstalter, auf GebĂŒhrenanteile (z.B. NachnenngebĂŒhr) zu verzichten, muss im + Abrechnungssystem einfach abbildbar sein. + +3. **Qualifikation von FunktionĂ€ren:** + Die Qualifikationen fĂŒr Richter und Parcoursbauer sind in der Datei `RICHT01.DAT` der ZNS-Daten enthalten. Diese + Qualifikationen mĂŒssen gegen die Anforderungen des jeweiligen Bewerbs (Klasse, Sparte) validiert werden. + +## đŸ‘· [Backend Developer] - Erkenntnisse + +1. **Bounded Context "Billing & Accounting":** + Die KassenfĂŒhrung ist zu komplex und hat zu viele eigene Regeln, um sie eng mit dem sportlichen Kern ( + Nennung/Ergebnis) zu koppeln. Wir werden dies als separaten Bounded Context mit eigenen Services und einem + dedizierten Datenmodell fĂŒr Konten, Transaktionen und GebĂŒhren umsetzen. Die Abrechnung muss kontobasiert pro Zahler + erfolgen (nicht nur pro Reiter). + +2. **Implementierung "Nennungstausch":** + Der Tausch einer Nennung (Reiter/Pferd) wird nicht als reines Storno + Neu-Buchung implementiert, sondern als * + *Transfer-Operation**. Bereits bezahlte Nenngelder werden als Guthaben auf dem Konto des ursprĂŒnglichen Zahlers (oder + des Pferdes) gefĂŒhrt und können auf die neue Nennung angerechnet werden. Das System berechnet und verbucht + automatisch die anfallenden Differenzen und TauschgebĂŒhren. + +3. **Validierungs-Service fĂŒr FunktionĂ€re:** + Es wird eine Backend-Funktion implementiert, die bei der Zuweisung eines Richters/Parcoursbauers zu einem Bewerb + dessen Qualifikationen (aus den importierten ZNS-Daten) gegen die Anforderungen des Bewerbs prĂŒft. Die API wird bei + einem Mismatch eine `WARNUNG` zurĂŒckgeben, aber keinen harten `FEHLER`, um den vom Rulebook Expert bestĂ€tigten " + Override" durch die Meldestelle zu ermöglichen. + +4. **ZNS-Import, Fremddaten (ZuchtverbĂ€nde) & Event Sourcing:** + Die unsauberen und oft wechselnden ZNS-Daten des OEPS werden nicht destruktiv in Datenbanktabellen ĂŒberschrieben. + Stattdessen wird eine **Event Sourcing** Architektur gewĂ€hlt. + * Das Hochladen der `zns.zip` triggert einen Parser, der Änderungen (Updates bei Lizenzen, neue Akteure) als + zeitgestempelte Events (z.B. `ActorUpdatedEvent`) in einem "Event Log" ablegt. + * Fremddaten (z.B. von ZuchtverbĂ€nden wie dem AWÖ) können ĂŒber dedizierte Parser eingelesen und in dieselben + Standard-Events ĂŒbersetzt werden. + * **Manuelle Korrekturen durch die Meldestelle** (wegen fehlerhafter OEPS-Daten) erzeugen "Override-Events", die + Vorrang vor veralteten Import-Daten haben. + * FĂŒr die schnelle Anzeige und Offline-Synchronisation werden aus diesen Events saubere Projektionen ("Read Models") + und turnierspezifische Snapshots gebaut. + +## 🎹 [Frontend Expert] & đŸ–Œïž [UI/UX Designer] - Erkenntnisse + +1. **Workflow-Übernahme als Grundlage:** + Die vom Owner bereitgestellten Screenshots des Altsystems (`BilderSuDo/`) definieren einen praxiserprobten, schnellen + und vom User extrem gut akzeptierten Workflow. Diese Screens dienen als absolute Blaupause fĂŒr das neue UI/UX-Design. + +2. **Der "Bewerbe anlegen"-Workflow (`Bewerbe.PNG` etc.):** + * Zweigeteilte Ansicht (Master-Detail): Links Liste/Filter aller Bewerbe, Rechts Detail-Tabs. + * Detail-Tabs gliedern sich in: *Bewertung* (Richtverfahren), *Geldpreis* (Dotierung nach Platzierung), *Ort/Zeit* ( + Ablaufplanung mit Zeit-pro-Starter-Logik). + * *Design-Vorgabe:* Diese Informationsarchitektur wird in moderne Compose-Layouts (z.B. List-Detail-Pane) ĂŒberfĂŒhrt, + die Logik bleibt identisch. + +3. **Die zentrale "Nennungs-Maske" (`Nennungen.PNG`):** + * Dies ist das **HerzstĂŒck der Meldestelle** ("Das Telefon lĂ€utet..."). + * Extrem schnelle Suche via Kopfnummer oder Name (mit sofortiger Auto-Completion). + * Anzeige der Meta-Daten (z.B. Besitzer) fĂŒr sofortige Identifikation. + * Direkte Zuweisung zu Bewerben via Klick aus einer Liste unten. + * BerĂŒcksichtigung von StartwĂŒnschen ("vorne", "hinten"). + * *Design-Vorgabe:* Diese Maske muss auf **absolute Tastatur- & Schnellbedienbarkeit** optimiert werden (Hotkeys, + Tab-Flow). Sie fungiert als Dashboard (Absprung zu Startliste, Kassa). + * **Self-Service Äquivalenz:** Das Webformular fĂŒr den Reiter nutzt im Hintergrund dieselbe Logik, um die Meldestelle + maximal zu entlasten. + +4. **Startlisten-Erstellung (`Startlisten.PNG`):** + * Schneller Wechsel zwischen Bewerben ĂŒber Nummern-Leiste oben (Tab-Ersatz). + * BerĂŒcksichtigung der beim Nennen erfassten WĂŒnsche ("vorne", "hinten"). + +5. **Ergebnis-Erfassung am Richterturm (`Ergebnisliste.PNG`):** + * Schnelleingabe-Maske optimiert fĂŒr den fließenden Ablauf. + * Oben: Gesamtergebnis / Unten: Startliste als Warteschlange / Mitte: Aktueller Reiter in Eingabe. + * Absoluter Fokus auf **"Enter & Tabulator"-Workflow**. Ein Mausklick darf fĂŒr den Standard-Ablauf nicht nötig sein. + * Spontane Abweichungen von der Startfolge (nĂ€chster Reiter kommt frĂŒher) mĂŒssen durch simplen Doppelklick auf die + Warteschlange lösbar sein. + * Entscheidung ĂŒber Anzahl der Platzierten bleibt flexibel und ist durch die Meldestelle ĂŒberschreibbar (ÖTO als + Richtlinie, Veranstalter hat letztes Wort). + +## đŸ—ïž [Lead Architect] - Strategischer Fokus (MVP Phase 1) + +Um zĂŒgig einen echten Mehrwert zu generieren, wird der Scope fĂŒr die erste Ausbaustufe (MVP) hart eingegrenzt: + +* **Turnier-Kategorien:** Fokus auf **C-NEU** und **C**. +* **Sparten:** Fokus ausschließlich auf **Dressur (D)** und **Springen (S)**. +* *BegrĂŒndung:* Diese Eingrenzung reduziert die initial benötigte KomplexitĂ€t der Regelwerks-Implementierung enorm, + deckt aber gleichzeitig das absolute "Brot-und-Butter"-GeschĂ€ft fĂŒr kleine bis mittlere Veranstalter ab. diff --git a/docs/02_Domain/03_Analysis/Legacy_Spec_Analysis_2026-01.md b/docs/02_Domain/03_Analysis/Legacy_Spec_Analysis_2026-01.md new file mode 100644 index 00000000..3f5bf4e5 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Legacy_Spec_Analysis_2026-01.md @@ -0,0 +1,80 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +--- +# Analyse der Legacy-Spezifikation (OEPS Pflichtenheft 2021 V2.4) + +* **Datum:** 2026-01-14 +* **Quelle:** `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Pflichtenheft_V2.4_2021-07-28.md` +* **Status:** Draft + +## 1. Zusammenfassung + +Das Pflichtenheft definiert das Datenaustauschformat zwischen Meldestellen-Software und dem OEPS (Österreichischer Pferdesportverband). Es ist die **maßgebliche technische Referenz** fĂŒr nationale Turniere in Österreich. Die Einhaltung dieser Spezifikation ist zwingend erforderlich, um Ergebnisse an den Verband zu melden. + +## 2. Kritische Datenfelder & Identifikatoren + +Die Analyse zeigt, dass das aktuelle `Core_Model` (`Overview.md`) zu generisch ist. FĂŒr den operativen Betrieb fehlen essenzielle Identifikatoren. + +### 2.1. Identifikation von Personen & Pferden +Das System verlĂ€sst sich nicht primĂ€r auf Namen, sondern auf **Satznummern**. + +* **Pferd:** + * `Satznummer` (10-stellig, numerisch): Der primĂ€re Key im OEPS-System. Muss zwingend persistiert und exportiert werden. + * `Kopfnummer` (4-stellig, alphanumerisch): + * National: Die permanente Registrierungsnummer beim OEPS. + * International: Eine turnierspezifische Startnummer. + * `Lebensnummer` (9-stellig): Zuchtnummer. Achtung: Bei auslĂ€ndischen Pferden oft generiert/fiktiv -> Nicht zur Suche geeignet! + * `FEI-Pass` vs. `FEI-ID`: Zwei getrennte Felder! + +* **Reiter:** + * `Satznummer` (6-stellig, numerisch): Der primĂ€re Key. + * `Lizenz` (z.B. "RD1", "R1"): Bestimmt die Startberechtigung in Klassen. + * `Startkarte`: Flag, ob die JahresgebĂŒhr bezahlt wurde. Ohne Startkarte keine Startberechtigung (außer Gastlizenzen). + +### 2.2. Turnier & Bewerbsstruktur +Die Struktur ist starrer als im aktuellen Modell angenommen. + +* **Turniernummer:** 5-stellig. +* **Bewerbe:** + * Haben eine 2-stellige Nummer (intern) UND eine 3-stellige Nummer (fĂŒr Turniere > 99 Bewerbe). + * **Abteilungen:** Ein Bewerb kann in Abteilungen (Abt. 1, Abt. 2...) unterteilt sein. Dies ist keine rein organisatorische Trennung, sondern datentechnisch relevant (Feld `ABTEILUNG`). + +## 3. Implizite GeschĂ€ftsregeln + +Aus den Datenfeldern lassen sich harte Business Rules ableiten: + +1. **Startberechtigung (Sperrliste):** Es gibt ein Flag `SPERRLISTE`. Wenn gesetzt, muss das System warnen/blockieren. Grund oft: Offene Forderungen. +2. **Nation-Logik (Gast vs. InlĂ€nder):** + * AuslĂ€nder mit österr. Lizenz + bezahlter Startkarte -> Startet fĂŒr österr. Verein -> Nation im Ergebnis = "AUT". + * AuslĂ€nder ohne Mitgliedschaft -> Gastreiter -> Nation = StaatsbĂŒrgerschaft (z.B. "GER"). + * *Konsequenz:* Die "Nation" eines Starts ist kontextabhĂ€ngig und nicht rein statisch am Reiter hĂ€ngend. +3. **Pferde-Status:** Pferde, fĂŒr die >3 Jahre keine GebĂŒhr bezahlt wurde, gelten als "nicht registriert" -> Neuanlage erforderlich. + +## 4. LĂŒcken im aktuellen Modell (Gap Analysis) + +| EntitĂ€t | Fehlendes Attribut / Konzept | Dringlichkeit | +| :--- | :--- | :--- | +| **Pferd** | `Satznummer` (OEPS-ID) | **Hoch** (Sync unmöglich ohne dies) | +| **Pferd** | Unterscheidung `Kopfnummer` (Permanent) vs. `Startnummer` (Turnier) | Mittel | +| **Akteur** | `Satznummer` (OEPS-ID) | **Hoch** | +| **Akteur** | `Startkarte` (Status) | Hoch (Validierung) | +| **Bewerb** | `Abteilung` (Sub-Struktur) | Mittel | +| **Ergebnis** | `Ausschluss-Typ` (Disqualifikation, Aufgabe, Ausschluss) | Mittel | +| **Ergebnis** | `Geldpreis` (Formatierung, WĂ€hrung ist implizit EUR) | Niedrig | + +## 5. Empfehlung fĂŒr das Datenmodell + +Wir mĂŒssen die EntitĂ€t `Akteur` in spezifische Rollen-Modelle ausdifferenzieren oder per Composition erweitern, da die Attribute fĂŒr Reiter (Lizenz, Startkarte) fĂŒr andere Akteure (Richter, Besitzer) irrelevant oder anders sind. + +**Vorschlag:** +* `Akteur` bleibt Basis (Name, Kontakt). +* `ReiterProfile` (Value Object / 1:1 Relation): EnthĂ€lt `Satznummer`, `Lizenz`, `Startkarte`, `Sperrvermerk`. +* `Pferd` erhĂ€lt `OEPS_Daten` (Value Object): `Satznummer`, `Kopfnummer`, `Lebensnummer`. + +## 6. Offene Fragen an den PO + +1. Wie gehen wir mit **internationalen Turnieren** (FEI) um? Das Pflichtenheft deutet an, dass auch hier OEPS-Formate genutzt werden ("Version 2.4 fĂŒr internationale Bewerbe"), aber die FEI hat eigene Formate. Welches ist fĂŒhrend? +2. Soll das System den **Import** der `zns.zip` (Stammdaten) unterstĂŒtzen? Das wĂ€re essenziell fĂŒr den Offline-Betrieb. +3. Wie strikt soll die **Validierung** sein? Soll das System eine Nennung *verhindern*, wenn die Startkarte fehlt, oder nur *warnen*? (RealitĂ€t: Oft wird vor Ort nachgezahlt). diff --git a/docs/02_Domain/03_Analysis/Non_Functional_Requirements_Draft.md b/docs/02_Domain/03_Analysis/Non_Functional_Requirements_Draft.md new file mode 100644 index 00000000..a215db79 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Non_Functional_Requirements_Draft.md @@ -0,0 +1,85 @@ +--- +type: ADR +status: DRAFT +owner: Lead Architect +--- +# Non-Functional Requirements (NFRs) - Phase 1 + +* **Status:** Draft +* **Fokus:** Offline-First Architektur, Robustheit, DatenintegritĂ€t + +--- + +## 1. Offline-FĂ€higkeit & Resilienz (Availability) + +Das System muss in einer Umgebung funktionieren, in der Netzwerkverbindungen unzuverlĂ€ssig oder nicht vorhanden sind (ReitstĂ€lle, lĂ€ndliche Gebiete). + +* **NFR-01: Local-First Prinzip** + * Alle Kernfunktionen (Nennung, Startlistenerstellung, Ergebniserfassung, Drucken) mĂŒssen **zu 100% ohne Netzwerkverbindung** ausfĂŒhrbar sein. + * Die lokale Datenbank (SQLite/SQLDelight) ist die primĂ€re Datenquelle fĂŒr das UI. + * Der Server dient lediglich als Synchronisations-Hub und Backup, nicht als Laufzeit-AbhĂ€ngigkeit. + +* **NFR-02: Synchronisation & Konfliktlösung** + * Sobald eine Verbindung besteht, mĂŒssen Daten im Hintergrund synchronisiert werden. + * **Konfliktstrategie:** Bei konkurrierenden Änderungen (z.B. zwei Richter Ă€ndern dasselbe Ergebnis) muss das System: + 1. Technische Konflikte automatisch lösen (z.B. "Last Write Wins" basierend auf prĂ€zisen Zeitstempeln). + 2. Fachliche Konflikte protokollieren und zur manuellen KlĂ€rung markieren. + +* **NFR-03: Wiederherstellung (Disaster Recovery)** + * Nach einem Absturz oder Stromausfall muss das System innerhalb von **< 30 Sekunden** wieder betriebsbereit sein. + * Kein Datenverlust von bereits bestĂ€tigten Eingaben (ACID-Transaktionen lokal). + +--- + +## 2. Performance & Latenz (Usability) + +Im Turnierbetrieb herrscht Zeitdruck. Wartezeiten summieren sich und fĂŒhren zu Stress bei den Anwendern. + +* **NFR-04: Optimistic UI Updates** + * Benutzeraktionen (z.B. Speichern einer Note) mĂŒssen im UI **sofort (< 50ms)** bestĂ€tigt werden, ohne auf Netzwerk-Roundtrips oder Datenbank-Commits zu warten (Asynchrone Verarbeitung). + +* **NFR-05: Such-Performance** + * Die Suche nach Pferden (in > 50.000 Stammdaten) oder Reitern muss **< 200ms** dauern (Full-Text-Search Indexierung lokal). + * Dies gilt auch auf leistungsschwĂ€cherer Hardware (Ă€ltere Laptops, Tablets). + +* **NFR-06: Massendaten-Verarbeitung** + * Der Import der `zns.zip` (Stammdaten) darf den UI-Thread nicht blockieren und sollte **< 5 Minuten** dauern. + +--- + +## 3. DatenintegritĂ€t & Audit (Compliance) + +Ergebnisse entscheiden ĂŒber Qualifikationen und Preisgelder. Manipulationen oder versehentliche Änderungen mĂŒssen nachvollziehbar sein. + +* **NFR-07: Audit Trail** + * Jede Änderung an einem Ergebnis (Score, Zeit, Platzierung) muss unverĂ€nderbar protokolliert werden. + * Inhalt: `Timestamp`, `User-ID`, `Old-Value`, `New-Value`, `Reason` (optional). + * Der Audit-Log muss mit synchronisiert werden. + +* **NFR-08: Validierungs-Hierarchie** + * Das System muss zwischen "Hard Constraints" (Datenbank-IntegritĂ€t, z.B. Foreign Keys) und "Soft Constraints" (Fachliche Regeln, z.B. fehlende Startkarte) unterscheiden. + * Soft Constraints dĂŒrfen den Prozess nicht blockieren, mĂŒssen aber persistente Warnungen erzeugen ("Override"-Flag). + +--- + +## 4. Sicherheit (Security) + +* **NFR-09: Lokale Datensicherheit** + * Da Laptops/Tablets gestohlen werden können: Sensible Daten (Personendaten, Adressen) sollten "At Rest" verschlĂŒsselt sein (z.B. SQLCipher), sofern die Performance (NFR-05) nicht kritisch beeintrĂ€chtigt wird. + * Minimalanforderung: Keine Speicherung von Passwörtern im Klartext. + +* **NFR-10: Rollenbasierter Zugriff (RBAC)** + * Unterscheidung der Berechtigungen im UI: + * *Richter:* Darf nur Ergebnisse fĂŒr zugewiesene Bewerbe eingeben. + * *Meldestelle:* Vollzugriff. + * *Zuschauer (Kiosk-Mode):* Nur Lesezugriff auf Starter-/Ergebnislisten. + +--- + +## 5. Hardware & Umgebung + +* **NFR-11: Eingabe-Effizienz** + * Die Ergebniserfassung muss "Keyboard-First" bedienbar sein (Nummernblock-Optimierung). Maus/Touch ist fĂŒr Massenerfassung zu langsam. + +* **NFR-12: Druck-UnterstĂŒtzung** + * UnterstĂŒtzung von lokalen Druckern (USB/Netzwerk) ohne komplexe Treiber-Installation, da Listen (Starterlisten, Ergebnisse) physisch ausgehĂ€ngt werden mĂŒssen (Pflicht laut Reglement). diff --git a/docs/02_Domain/03_Analysis/Scenarios/Anekdote_Meldestelle.md b/docs/02_Domain/03_Analysis/Scenarios/Anekdote_Meldestelle.md new file mode 100644 index 00000000..8f441c13 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Scenarios/Anekdote_Meldestelle.md @@ -0,0 +1,697 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +--- +# Anekdote Meldestelle + +Ich bin diesmal die Meldestelle fĂŒr ein kleines Turnier, z.B. ein "CDN-C Neu" bzw. "CSN-C Neu" am "Musterhof". + +Sagen wir in der Ausschreibung stand "Meldestelle geöffnet ab 15 Uhr", das Turnier findet Samstag und Sonntag statt. + +Also, am Freitag mache ich mich auf den Weg zum Turnier, aus Erfahrung achte ich darauf, dass ich mehr als rechtzeitig am TurniergelĂ€nde eintreffe. + +Meistens bin ich schon ca. 2-3 Stunden vor der offiziellen Öffnungszeit vor Ort. + +Also treffe ich ca. 12 Uhr am TurniergelĂ€nde ein. + +"Hallo? Ist hier jemand?" + +Ich mache mich auf die Suche nach den Verantwortlichen, oder irgendjemanden, der mir sagen kann, wo ich meine "Meldestelle" aufbauen kann. + +Da ein ein Stallbursche, er spricht kaum deutsch und kein englisch, aber mit HĂ€nden und FĂŒĂŸen haben wir uns dann doch noch verstĂ€ndigen können und er weis jetzt, wonach ich suche. + +Leider hat er auch keine Ahnung wo ich meine "Meldestelle" aufbauen kann. + +Jetzt machen wir uns zu zweit auf die Suche nach einer Person, die mir weiterhelfen kann. + +Endlich, nach einer gefĂŒhlten Stunde auf der Suche, haben wir ein Vereinsmitglied gefunden, das mir sagen kann, wo ich meine "Meldestelle" aufbauen kann. + +Echt, jetzt? + +Im letzten Winkel vom TurniergelĂ€nde in einem schĂ€bigen alten HolzhĂ€uschen, das so aussieht und die GrĂ¶ĂŸe ist wie ein "Plumpsklo" auf einer Alm-HĂŒtte. + +Naja, zumindest habe ich so was Ähnliches wie einen Schreibtisch und einen Klappsessel. + +Ich: "Karin, könnte ich vielleicht noch Strom haben?" + +Karin ist das Vereinsmitglied, sie ist in dem Verein die "KassenfĂŒhrerin". + +Karin: "Ah, ja, muss nur schauen woher. Ich kĂŒmmere mich darum." + +Ich: "Danke, dass wĂ€re sehr hilfreich" + +Okay, Karin ist auf der Suche nach dem Strom. + +Zum GlĂŒck habe ich meine keine USV mitgenommen, damit ich zumindest kleine StromausfĂ€lle kompensieren kann. + +Das "Plumpsklo" ist noch staubig, also reinige ich ein wenig, bevor ich meinen Laptop aufstellen kann. + +Es ist mittlerweile schon 14 Uhr. + +Hmmm, wie soll ich jetzt ein Netzwerk aufbauen zum Richterturm? + +Was habe ich mit als Meldestelle: + +\- einen Internet-WĂŒrfel von A1 + +\- 3x Laptops Meldestelle Rechenstelle und Richter (bei kleinen Turnieren ist nur gemeinsames Richten, bei Springturnieren klopft der Zeitnehmer die Ergebnisse ein) + +\- 2x Switches + +\- 2x 50m Outdoor-Netzwerk-Kabeln + +\- 2x WLAN-Outdoor Access-Point von tplin CPE210 Sender EmpfĂ€nger + +\- einige 1-2 m Netzwerk-Kabeln + +\- einen Laserdrucker-SW \+ Papier + +\- 1x USV EATON 3S700D fĂŒr die Meldestelle + +Geht sich das noch aus, bevor ich die Meldestelle offiziell öffnen muss? + +Schauen wir mal, wo der Richter-Turm ist. + +War ja klar, der Austragungsplatz ist genau hinter dem StallgebĂ€ude und die Reithalle genau am anderen Ende des GelĂ€ndes. + +Wo kann ich meine Accesspoints montieren, ohne im Weg zu sein mit dem Kabel? + +WĂ€hrend ich so am GelĂ€nde herumlaufe und mir Gedanken mache, wie ich das Netzwerk aufbauen kann, lĂ€uft mir ein Reiter entgegen. + +"Hallo, du\! Bist du die Meldestelle?" + +Ich: "Ja, servus" + +Reiterin: "Kann ich schon Nennen?" + +Ich: "Ja sicher, im Internet. Brauchst nur den Link zum Online-Nennen anklicken." + +Reiterin: "Kann ich es nicht gleich persönlich bei dir machen? Ich habe kein Download-Guthaben mehr und das Internet funktioniert hier nicht wirklich" + +Ich: "Okay, komm mit, ich muss nur noch den Computer hochfahren" + +Reiterin: "Super, danke" + +Bei meinem "Plumpsklo". + +Ich schalte den Computer ein, natĂŒrlich noch im Akku-Betrieb, denn Strom habe ich noch immer nicht. + +Ich: "Okay, kann los gehen. Kopfnummer bitte?" + +Reiterin: "Kopfnummer? Die habe ich nicht." + +Ich: "Okay, welchen Bewerb möchtest du denn gehen?" + +Reiterin: "Reiternadel" + +Ich: "Ah, okay. FĂŒr diesen Bewerb muss das Pferd nicht registriert sein. Wie ist denn der Name des Pferdes?" + +Reiterin: "Also, wir nennen Ihn immer nur Hansi, aber ob das sein richtiger Name ist weis ich nicht" + +Ich: "Okay, weißt du was? Ich nenne dich jetzt einmal in den Bewerb mit dem Pferd Hansi und du fragst einmal nach, wie er wirklich heißt, okay?" + +Reiterin: "Super, danke" + +Ich lege ein neues Pferd an mit dem Namen "Hansi", sonst habe ich keine Infos. + +Das Programm erstellt mir eine selbst generierte Kopfnummer aus z.B: "C001" und dann fortlaufend fĂŒr alle neu erstellten Pferde, die nicht Turnierpferde registriert sind und nur fĂŒr Bewerbe, die keine erfordern. + +So, jetzt habe ich ein Pferd namens Hansi mit der Kopfnummer C001 im System. + +Ich: "Wie ist dein Name?" + +Reiterin: "Ich? Susi Jur..." + +Ich: "Okay, tut mir leid, ich habe deinen Nachnamen nicht ganz verstanden. Kannst du mir deinen Nachnamen bitte noch einmal wiederholen?" + +Reiterin: "Juritschenkowz" + +Ich: "Juri.. wie weiter? Kannst du mir deinen Namen bitte Buchstabieren?" + +Reiterin: "J U R I T sch" + +Ich: "das "sch" Siegfried CĂ€sar Heinrich ? Oder schreibt man es anders?" + +Reiterin: "Ich heiße nicht Siegfried." Ich heiße Susi" + +Ich: "Bist du bitte so freundlich und kannst mir hier deinen kompletten Namen aufschreiben, ich glaube, somit tun wir uns am leichtesten." + +Reiterin: "Okay, wars das jetzt, oder muss ich noch etwas tun?" + +Ich: "Nein, alles okay. Du kannst dann heute Abend so gegen 20 Uhr die Startlisten aus www.XXX.at abrufen, okay?" + +Reiterin: "So spĂ€t?" + +Ich: "Ja, ich habe noch einen langen Tag" + +Reiterin: "Danke, bis morgen" + +Susi geht zufrieden weiter, ich suche nach ihrem Namen im System. + +Fehlanzeige, keine Juri... in den ZNS-Daten, also lege ich die Reiterin einmal so an, wenn Sie wieder kommt werde ich hoffentlich die Daten vervollstĂ€ndigen können, denn mit diesem Ergebnis vom Turnier könnte Sie auch die Reiter-Lizenz erreiten. Voraussetzung natĂŒrlich, dass die Daten vollstĂ€ndig und korrekt sind, gilt ĂŒbrigens auch fĂŒr Reiter aus Deutschland, dass diese Ergebnisse in Ihrem Verband anerkannt werden. + +Okay, 14:30 Uhr + +Na super, Netzwerk habe ich noch nicht aufgebaut und Zeit dafĂŒr habe ich auch keine mehr dafĂŒr. + +Meldestellen-Handy, stimmt die Nummer in der Ausschreibung mit dem Handy ĂŒberhaupt ĂŒberein? + +Probe: 0664 12 12 123 + +Ja, Akku ist voll, ich bin mit Handy und Laptop bereit. + +Karin taucht auf. + +Karin: "Ich hab deinen Strom\!" + +Ich: "Super" + +Karin: "Brauchst du noch was?" + +Ich: "Vielleicht einen Kaffee?" + +Karin: "Muss ich schauen ob wir schon einen haben" + +Ich: "Dass, wĂ€re echt toll" + +Was will man mehr? + +Frische Luft im Plumpsklo neben dem Misthaufen, Strom, aja, habe ich ĂŒberhaupt Internet? + +Mal schauen, okay bisschen, speed-test download gerade einmal 10MB/s + +Handyempfang schlecht, aber vorhanden. + +ca. 14:40 Uhr + +Ring ring Meldestellen HĂ€ndy lĂ€utet + +Ich: "Meldestelle, Musterhof Guten Tag" + +Anrufer: "Ah, bist es nicht du Matthias?" + +Ich: "Nein, ich bin es Mo dieses Wochenende mach ich die Meldestelle am Musterhof" + +Anrufer: "Ah, okay. Tragst du mich mit meinen Pferden in die A's und L's ein?" + +Ich: "Ah, du bist es, Petra hab dich nicht gleich erkannt. Der Empfang hier ist ziemlich schlecht. Welche Pferde hast du denn mit?" + +Petra: "Tschuldigung, servus Mo\! Ja, ich habe Seppi, Weißdorn und die Obora mit. Weißt eh welche Reihenfolge” +Ich: “Ich glaube, das werde ich schon schaffen” +Petra: “Danke, bist spĂ€ter\!” + +Okay, Petra, wie war noch Ihr Nachnahme Sie hat ja vor kurzen geheiratet. Aja, Sie heist jetzt Musterfrau. +Seppi, den kenne ich, aber wie war noch der Turniername? Sebastian, ja da habe ich Ihn ja, “Sebastian Von Seebenstein” A357, das ist der junge mit dem will Sie sicher nur die A-Bewerbe gehen. +Weißdorn, da haben wir Sie ja “Weißdorn by Gewizda” 8936, in die L-Bewerbe. +Und dann noch “Tuchlaub by Oboras” 9056, ebenfalls in die L-Bewerbe. +Erledigt. + +Ring ring 
 + +Das geht jetzt sicher 1-2 Stunden so weiter. + +17:30 Uhr + +Mal schauen wie viele Starter wir schon habe, okay fĂŒr das “CSN-C Neu” haben wir schon 150 Starter. FĂŒr das kleine Turnier gar nicht schlecht. +Dressur sieht noch etwas mager aus, mit derzeit nur 23 Startern. +Naja, was will man machen, ist halt so. + +Zeitplan? +Ist der Parcoursbauer schon da? Der Markus ist doch normalerweise immer frĂŒher da, um den Parcours fĂŒr den ersten Bewerb aufzustellen. + +Karin, lĂ€uft wieder bei mir vorbei. +Ich: “Karin\! Hast du vielleicht schon den Markus gesehen?” +Karin: “Markus wer?” +Ich: “Der Parcoursbauer” +Karin: “Acht, den meinst du. Nein, er hat angerufen und hat gemeint, er kommt morgen gleich ganz in der FrĂŒh.” +Ich: “Danke\!” + +Wieder typisch fĂŒr Ihn, immer alles auf den letzten DrĂŒcker. +Okay, zum planen, wie lange wird ein Reiter brauchen? +Die ersten Bewerbe, sind welche fĂŒr AnfĂ€nger oder junge Pferde, denen geben wir mehr Zeit. +Countdown: 45 Sek. +Parcours: 350 m/min Erlaubte Zeit: 60 Sek. Idealzeit: \-10% \= 54 Sek. +sagen wir zum Kalkulieren in den ersten Bewerbe (1 \- 5\) 2 min./Starter +Siegerehrungen gibt es keine, sondern nur Mascherln (Schleifen) bei 0 Fehler direkt beim Ausritt vom Bewerbsplatz. +Wir haben nicht so viele Starter, das wird schon passen. +Eine kleine Mittagspause fĂŒr die Richter, sagen wir 15 min. +zwischen den Bewerben Umbauzeit sagen wir 10 min., Besichtigung 5 min. +Das passt schön, Bewerbe 1, 2, 3, 4, 5 sind zur zeit 80 Starter +macht pro Bewerb \+ Umbauzeit \+ Besichtigen 55 min. ca. 1 Std. +Haben wir dann um ca. 13 Uhr Mittagspause von 15 min. +Zeit fĂŒr Bodenpflege und BewĂ€ssern. +Machen wir um 13:30 Uhr mit den nĂ€chsten Bewerben weiter. +Bewerb 6, 7, 8, 9 normal mit kleiner Siegerehrung ca. 10 min. +Bewerb 10 ist der letzte und der Hauptbewerb des Tages. +Das heißt wieder kleine Bodenpflege davor +Bewerbe 6 \- 9 habe ich 49 Starter \~ 50 ĂĄ 1 min 30 sek. +13:30 Uhr \+ 2 h 15 min \= 15:45 Uhr \~ 16:00 Uhr +Also ca. 16 Uhr Hauptbewerb. + +Wie spĂ€t ist es? + +18:30 Uhr + +Okay, machen wir einmal einen VorlĂ€ufigen Zeitplan fĂŒr die Dressur. +Da haben wir zum GlĂŒck nur 5 Bewerbe morgen. +Wie viele Starter? Gesamt 46 Starter +1 D-Bewerb Aufgabe XY ca. 3 min 30 sek \~ 4 min Starter 6 +45 sek Countdown, AnfĂ€nger machen wir 5 min / Starter Beginn 8:00 Uhr Endet: 8:30 Uhr +In der Dressur hat jeder Starter seine fixe Startzeit. +kleine Siegerehrung 10 min +Startzeit erster Bewerb 8:00 Uhr +2 D-Bewerb Aufgabe XY ca. 3 min 30 sek \~ 4 min Starter 10 +45 sek Countdown, AnfĂ€nger machen wir 5 min / Starter Beginn: 9:00 Uhr Endet: 9:50 Uhr +kleine Siegerehrung 10 min +3 D-Bewerb Aufgabe XY ca. 4 min 30 sek \~ 5 min Starter 10 +45 sek Countdown, AnfĂ€nger machen wir 6 min / Starter Beginn: 10:15 Uhr Endet: 11:15 Uhr +kleine Siegerehrung 10 min +4 D-Bewerb Aufgabe XY ca. 4 min 30 sek \~ 5 min Starter 5 +45 sek Countdown, AnfĂ€nger machen wir 6 min / Starter Beginn: 12:00 Uhr Endet: 12:30 Uhr +kleine Siegerehrung 10 min +5 D-Bewerb Aufgabe XY ca. 5 min \~ 6 min Starter 15 +45 sek Countdown, AnfĂ€nger machen wir 6 min / Starter Beginn: 13:00 Uhr Endet: 14:30 Uhr + +Ich glaube, es sieht gut aus. + +Uhrzeit? + +18:45 Uhr + +Jetzt ruf ich mal den Michi (Veranstalter) an, dass er mal her kommen soll, damit er sich den Zeitplan ansehen kann, ob dieser ihm passt. + +Ich: “Servus, Michi, kommst einmal vorbei? wegen Zeitplan” +Michi: “Ja, komme gleich, magst auch ein Bier?” +Ich: “Ja, gerne” + +\====================================================================== + +Dem Michi hat mein Zeitplan gefallen. +Nachdem ich die Startlisten dann noch sortiert hatte nach WĂŒnschen der Reiter und wenn Reiter mehrere Pferde am Start hatten, habe ich dies natĂŒrlich auch berĂŒcksichtigt und diese dann je nach Wunsch ganz nach vorn und das zweite Pferd ganz nach hinten in der Startreihenfolge geplant. +Manche Trainer wollen alle Ihre SchĂŒtzlinge zusammen haben, wofĂŒr ein optionaler Vermerk auf den Trainer machen könnte, sicher ein Plus. + +\===================================================================== + +Michi kommt mit meinem Bier zu meinem Plumpsklo, alias Meldestelle. +Michi: “Servus, Mo\! Hast eh alles gefunden, oder?” +Ich: “Ja, soweit alles klar, du musst mir nur noch helfen mein Netzwerk fertig aufzubauen.” +Michi: “Was brauchst den dafĂŒr?” +Ich: “Meine beiden Funkies mĂŒssen noch so aufgehĂ€ngt werden, damit sich die beiden sehen können. Womöglich so hoch wie möglich, damit die Handys den Funk nicht stören.” +Michi: “Ah, das kann Janosch machen, mein Stallbursche, der kennt sich mit allem aus, der macht das schon. Ich rufe ihn gleich her, dann kannst du ihm die Funkies gleich geben.” +Ich: “Super, eine Sorge weniger. Du, wegen dem Zeitplan. Ich habe dir eine kleine Mittagspause eingeplant so gegen 13:15 Uhr, passt das so fĂŒr dich?” +Michi: “Ja, passt schon, zeig her mal den Plan.” + +Michi sieht sich den Plan an. FĂŒnf Minuten spĂ€ter sieht er mich an. +Michi: “Wieso beginnen wir schon um 8 Uhr FrĂŒh? Wenn wir eh nicht so viele Starter haben?” +Ich: “Weil du es so in der Ausschreibung schon so veröffentlicht hast?” +Michi: “Na okay. Wie viele PfefferstĂ€ser haben wir denn morgen?” (Damit meint Michi die Dressurreiter) +Ich: “ Steht eh drauf, 46” + +In der Zwischenzeit ist auch schon Janosch aufgetaucht. + +Michi: “Joschi, nimm dir die zwei Funkies und montiere sie, okay?” +Janosch: “Chef, ja Chef” + +Janosch sieht sich die WLAN-Funkies an, sieht mich an + +Janosch: “Deine? Gute Funky, ich kennen” + +und grinst mich an. Erhobenen Daumens grinse ich zurĂŒck + +Ich: “Janosch, da” und zeige Ihm die Beschriftung auf den Funkies +“Blaue Schrift ist Meldestelle und Rote Schrift ist Richter” +Janosch: “Gut Chefe, mach ich” + +Okay, Janosch macht sich an die Arbeit, mein Netzwerk fertig aufzubauen und Michi ist wieder, keine Ahnung was er macht. + +19 Uhr \- Nennschluss + +So, jetzt kann ich endlich die Startlisten fertig machen. Die Springer mach ich zuerst, mit denen bin ich schneller fertig. +Ausschreibung, wo habe ich Sie? Ach ja, hier. +Die Bewerbe einmal in die einzelnen Abteilungen trennen. +Die ersten Bewerbe sind die “AnfĂ€nger-Bewerbe”, haben sich da hoffentlich keine Lizenz-Reiter eingeschmuggelt? +Da, natĂŒrlich, was mach ich denn jetzt? +Lizenz-Reiter mit den lizenzfreien Reitern gemeinsam zu beurteilen ist nicht fair. +Ich mache noch eine Abteilung auf, das muss ich Franz sagen. (Franz ist der Turnier-Beauftragter Richter fĂŒr dieses Turnier) +Bewerb 6, 7 und 8 habe ich einige Reiter dabei die 2 und 3 Pferde reiten, diese Bewerbe werde ich fĂŒr die Startlisten noch nicht in die einzelnen Abteilungen aufteilen, sonst haben diese Reiter nicht genĂŒgend Zeit Ihre Pferde vorzubereiten und oder wir brauchen zu viel Zeit auf diese dann zu warten. +Ich lasse diese Bewerbe gemischt und teile erst am Ende des jeweiligen Bewerbes die Ergebnisse in die richtigen Abteilungen auf. +Im Hauptbewerb darf ich das natĂŒrlich nicht, oder wenn es um Geld geht oder Meisterschaft. +Den Rest lasse ich einfach in alphabetischer Reihenfolge nach Reiter-Nachname sortieren. +Bei Meisterschaften oder Cups könnte ich die Reihenfolge nach Zufallsprinzip sortieren. +Manche Veranstalter wollen bei PferdeprĂŒfungen die Sortierung nach Pferdename oder Abstammung haben. +Bei Haflinger Bewerben ist es einfach zu ĂŒberprĂŒfen, ob tatsĂ€chlich nur Haflinger auf die Startliste gefunden haben, denn die Kopfnummern der Haflinger beginnen alle mit einem “H”. +Und rein theoretisch beginnen alle österreichischen Warmblutpferde mit einem “A”. +Das Allerbeste ist, dass die Bewerbe auch manuell in Abteilungen fĂŒr jeden einzelnen Starter aufgeteilt werden können, denn nicht immer sind die Bewerbe nach gĂ€ngigen Regeln geteilt. +Bei diesem Turnier sind die Bewerbe zum GlĂŒck nur der Norm entsprechend nach Reiter-Lizenzen und Pferdealter in Abteilungen getrennt und die RS4-Reiter werden sowieso separat gewertet. + +Startlisten springen fertig, jetzt kann ich die ersten Startlisten veröffentlichen. + +Weiter zu den Dressur-Reitern, da sind etliche dabei die extra WĂŒnsche haben. +Ich darf nicht vergessen, die Susi nach weiteren Informationen zu Ihrem Pferd zu fangen. +Da, schon wieder eine Reiterin, die darum bittet ganz hinten starten zu wollen, weil angeblich Ihr Pferd sonst zu sehr abgelenkt wird. +Ich glaube, Sie will noch ein wenig lĂ€nger schlafen. +Petra hat viele Pferde mit auf dem Turnier und noch mehr SchĂŒler. +Sie will immer einen SchĂŒler von Ihr, dann einen anderen dazwischen und dann wieder einen SchĂŒler von Ihr. +Mal sehen, ob das ĂŒberhaupt so ausgeht. +Bei den Dressur-Reitern muss ich besonders acht geben, denn diese bestehen auf Ihre Startzeiten der Startlisten welche heute Abend veröffentlicht werden und diese dĂŒrfen nicht mehr so leicht geĂ€ndert werden. +Auch wenn vor Ihnen 2-3 Reiter ausfallen wĂŒrden und Sie der/die letzte Starter des Bewerbes sind, kommen die wenigsten Dressurreiter auf die Idee auch nur 5 Minuten frĂŒher an den Start zu gehen, denn diese Zeit brauchen Sie ja um sich selbst und Ihr Pferd auf den Punkt vorzubereiten. +Ob das den Unterschied macht, wage ich zu bezweifeln. + +Okay, Startlisten Dressur fertig, veröffentlichten. + +So, die Protokolle fĂŒr die Springreiter ausdrucken, die Startlisten fĂŒr den Vorbereitungsplatz, Turnier-Sprecher und das Richterkollegium drucke ich erst morgen in der frĂŒh aus, ca. eine Stunde vor dem Bewerbs beginn, denn dann sollten absolut keine Änderungen möglich sein, normalerweise. +Springreiter sind diesbezĂŒglich, vor allem auf kleinen nationalen Turnieren ein Kapitel fĂŒr sich. Denn einige von Ihnen kommen noch 5 min. vor dem Bewerbs-Beginn auf die Idee, doch noch eine Änderung vornehmen zu wollen. +Reihenfolge der Pferde, wenn diese 2 oder mehr Pferde im Bewerb haben. +Oder, weil es im Vorbewerb nicht so gut lief, noch schnell auch den nĂ€chsten Bewerb starten zu wollen. +Bei manchen Heros muss man glĂŒcklich sein, wenn Sie ĂŒberhaupt in der Meldestelle bescheid geben lassen, oft muss der Zeitnehmer verdammt aufpassen, ob es tatsĂ€chlich des richtige Pferd-Reiter-Paar ist, damit er das Ergebniss dem richtig Paar zu gute bucht. +Wie es oft am Richterturm zugeht, wissen wir ja. + +Ring Ring 
 + +Es ist mittlerweile 19:30 Uhr + +Habe ich jemanden in die falsche Abteilung oder Bewerb eingetragen? + +Ich: “Meldestelle, Musterhof” +Anrufer: “Hallo, ich habe ganz vergessen zu Nennen, kannst du mich noch eintragen?” +Ich: “Okay, Kopfnummer 
” + +War ja klar, dass mindestens ein Teilnehmer nach Nennschluss anruft, um sich noch schnell nennen zu lassen. + +Ring Ring 
 + +Noch was? + +Ich: “Meldestelle, Musterhof” +Anrufer: “Du hast mich in den falschen Bewerb eingetragen” +Ich: “Okay, wer bist du und was habe ich falsch gemacht?” + +19:45 Uhr + +Aber jetzt kann ich endlich die Protokolle fĂŒr die Dressur vorbereiten. + +Okay, ich brauche die Dressur-Aufgaben, die LeitfĂ€den und Startlisten pro Bewerb. +Bei diesem Turnier ist nur gemeinsames Richtverfahren auf einem 20x40 m Viereck ausgeschrieben. +Pickerln werde ich noch drucken, damit die “Schreiber” ein wenig entlastet werden, um die Dressur-Protokolle zu personalisieren. + +20:15 Uhr + +Mal nachsehen, was Janosch gemacht hat mit meinem Netzwerk. +Ja, das hat er wirklich gut montiert. Machen wir einen Testdurchlauf ob die Verbindung zum Richterturm Dressur und Springen funktioniert. + +Super alles funktioniert wie es soll. + +\==== NĂ€chster Morgen 6:30 Uhr \==== + +So geht es auf einen neuen Turniertag. + +\* Strom \- check +\* Meldestellen-Laptop inkl. Programm \- check +\* Meldestellen-Handy aufgeladen und bereit \- check +\* Internet \- check +\* Startlisten angeschlagen \- check +\* Spring-Protokolle Standard und Stil Zeitplan und fĂŒr den ersten Bewerb die Startlisten fĂŒr Richter, Sprecher und Abreiteplatz vorbereitet \- check +\* Dressur-Protokolle und Pickerln fĂŒr den ersten Bewerb, Zeitplan fĂŒr Richter, Sprecher und Abreiteplatz vorbereitet \- check +\* Kontrollgang Richterturm-Springen: +\- Strom \- check +\- Laptop inkl. Programm einsatzbereit \- check +\* Kontrollgang Richterturm-Dressur: +\- Strom \- check +\- Laptop inkl. Programm einsatzbereit \- check + +7:00 Uhr + +Ich glaube, von meiner Seite ist alles Einsatzbereit. + +Starterin: “Hast du eine Aufgabe fĂŒr mich?” +Ich: “Guten Morgen\! Welche Aufgabe hĂ€ttest du denn gerne?” +Starterin: “Na die, die jetzt gleich dran kommt” +Ich: “Aufgabe Reiterpass. Bitte gerne” + +Tutro (AbkĂŒrzung fĂŒr Turnier-Trottel, eine liebevolle Bezeichnung fĂŒr einen Turnier-Helfer, meistens Freunde aus dem Verein welche einem als Turnier-Starter helfen um z.B: Startlisten zu besorgen und den Überblick behalten bezĂŒglich der Startzeit und so weiter, damit man sich als Teilnehmer voll und ganz auf sich selbst und sein Pferd konzentrieren kann.) + +nĂ€chster Turto: “Hast du fĂŒr mich eine Startliste, fĂŒr die nĂ€chsten beiden Spring-Bewerbe?” +Ich: “Da habe ich ein paar aufgelegt zur freien entnahme” + +Franz: “Guten Morgen Mo\! Danke, fĂŒr die Info gestern am Abend mit dem Zeitplan. Die Richter-Einteilung” +und ĂŒberreicht mir einen Schmierzettel mit fast undefinierten Hieroglyphen +Ich: “Guten Morgen, Herr Turnier-Beauftragter Richter. Haben der Herr gut geschlafen?” +Franz: “Weist schon gibt\`s an Kaffee?” +Ich: “Musst schauen, ich hab nur meinen Löskaffee. Magst einen von mir?" +Franz: “Na, den kannst da kalten. Hast schon was fĂŒr’n Narrenturm?” +Ich: “Ist schon alles oben” + +Franz macht sich auf den weg zum Narrenturm, Richterturm. + +“Hallo, ich bin Anna. Ich soll die Protokolle fĂŒr die Dressur von dir holen.” +Ich: “Guten Morgen, Anna. WofĂŒr brauchst du denn die Protokolle?” +Anna: “Ich bin die Schreiberin” +Ich: “Ah, sehr gut. Ich habe schon alles fĂŒr dich vorbereitet, musst nur noch die Pickerln kleben und beim schreiben immer aufpassen, ob du eh das richtige Protokoll zum richtigen Starter hast, okay? Startlisten und Zeitplan hast du auch schon mit dabei.” +Anna: “Pickerln, ich muss nicht alles mit der Hand vorschreiben? Das ist ja cool” +Ich: “und kennst du dich schon mit meinem Programm aus um die Wertungen gleich in den Computer klopfen kannst?” +Anna: “Was, das muss ich auch noch machen?” +Ich: “Das ist nicht schwer, auf den Bewerb klicken, den richtigen Starter anklicken und die Wertung eingeben, enter, enter und der nĂ€chste Starter laut Startliste die du unten sehen kannst wird rein geladen. Die Wertung eingeben, enter, enter und so weiter. +Also ganz einfach und unkompliziert.” +Anna: “Und was ist wenn ich mal einen Fehler mache?” +Ich: “Kein, Problem. du klickst den Starter aus der Ergebnis-Liste, die du gleich oben sehen kannst an, somit hast du in wieder zurĂŒck geladen, besserst die Wertung aus, bestĂ€tigst erneut, und das war es auch schon. Die Ergebnisse werde ich spĂ€ter auch noch mit dem schriftlichen Protokoll (Die Startliste wird gerne als “Notiz-Zettel” benutzt auf dem die Richter Ihre Wertungen selber aufschreiben, vom Hauptrichter des Bewerbes nehmen wir mit in die “Ablage”) der Richter ĂŒberprĂŒfen. Also keine Sorge, das machst du schon.” +Anna: “Und warum, muss ich die Wertung auch noch eingeben?” +Ich: “Damit informieren wir die Starter, die Ergebnisse sind live im Internet. Achja und ich in der Meldestelle werde meistens nicht darĂŒber informiert, wie viele Starter dann tatsĂ€chlich platziert werden. Das Programm schlĂ€gt dir nur so viele Platzierungen vor, wie es die ÖTO vorsieht, aber wie viele dann wirklich platziert werden, entscheidet der Veranstalter in Absprache mit dem Richterkollegium. Soweit ich Michi kenne will es sicher alle platzieren die positiv sind. Diese Einstellung fĂŒr die Platzierungen machst du bitte auch fĂŒr mich rechts oben, einfach die Anzahl oder die Prozent eingeben, okay?” +Anna: “Was, wir haben Live-Ergebnisse im Internet? Ist ja cool” +Ich: “Wenn du Fragen hast oder sonnst irgendein Problem hast, dann kannst du mich im Chat vom Programm einfach fragen.” +Anna: “Cool” + +Anna, schafft das bestimmt. + +7:30 Uhr + +Jetzt brauche ich nur noch die Parcours-Skizzen von Markus. +Karin ist auch schon frĂŒh unterwegs und lĂ€uft geradewegs bei mir vorbei. + +Ich: “Karin\!” +Karin sieht mich an. +Karin: “Ja?” +Ich: “Guten Morgen, bist auch schon frĂŒh unterwegs” +Karin: “Guten Morgen, kennst ja Michi, wenn ich nicht alles fĂŒr Ihn organisiere passiert hier nichts.” +Ich: “Apropo organisieren, hast du Markus schon gesehen? Ich brĂ€uchte noch seine Parcours-Skizzen.” +Karin: “Ja, er baut gerade auf” +Ich: “Eh schon. Wenn du Ihn siehst, sag Ihm er soll mir seine Skizzen bringen.” +Karin: “Mach ich” + +Auf den Parcours-Skizzen der Parcoursbauer bekomme ich die Informationen: +\- LĂ€nge der Bahn +\- Geschwindigkeit \- meistens 350 meter pro Minute +\- Anzahl der Hindernisse +\- Anzahl der SprĂŒnge +\- Erlaubte Zeit +\- Idealzeit +\- Start- und Ziel- Linie +\- usw. + +Viele Reiter hĂ€tten gerne so eine Skizze, damit Sie sich auf den Parcours vorbereiten können und wenn ich diesen online stellen kann, sind alle zufrieden. + +“Hallo, ich bin die Schreiberin fĂŒr das Springen” +Ich: “Guten Morgen, hat Michi diesmal keine Zeitnehmung?” +“Nein, er hat gesagt fĂŒr das kleine Turnier braucht er keine” +Ich: “Okay, ich bin Mo, wer bist du?” +“Ah, ich bin Sissi” +Ich: “Servus, Sissi\! Es sollte schon alles oben sein und kennst du dich schon aus mit dem Programm?” +Sissi: “Ja, letzte Woche habe ich das auch schon gemacht. Da war Matthias am StĂŒcklerhof” +Ich: “Sehr gut, und war es schwer?” +Sissi: “Nein, aber die Übersicht könnte noch etwas besser sein” +Ich: “Okay, kannst du mir einen Vorschlag machen, wie du dir das Vorstellst? Indem du mir eine Beschreibung geben kannst oder eine Zeichnung?” +Sissi: “Ja, mach ich” +Ich: “Super, danke” + +Ein weiterer Tutro: “Hast du die Nummer vom Hufschmied” +Ich: “Die steht gleich an der Anschlag-Tafel. Was ist passiert” +Turto: “Unser Pferd hat sich gerade am Abreiteplatz ein Eisen runter getreten” +Ich: “Welches Pferd?” + +Damit ich im Programm diesen Starter markieren kann, dass ein Problem gemeldet wurde. Somit wird der Schreiber/Zeitnehmer auf der Startliste seines Sichtfensters informiert, dass es ein Problem bei diesem Starter gibt. Diese Informationen kann dieser dann am Richterturm bescheid geben. FĂŒr einen schnellen, einfachen Informationsaustausch zwischen Meldestelle und Richterturm bzw. direkt an den oder die zustĂ€ndigen FunktionĂ€re. + +Tutro: “Das Pferd vom Daniel” +Ich: “Sag mir oder dem Richterturm bescheid, ob Ihr noch an den Start gehen könnt\!” +Tutro: “Mach ich” + +FĂŒr solche oder Ă€hnliche Probleme am Abreiteplatz, wĂ€re eine direkte Verbindung vom Abreiteplatz zur Meldestelle und zum Richterturm bzw. an FunktionĂ€re sicher ein Plus. +Könnte auch hilfreich sein, wenn diese Live-Ergebnisse im eigenen LAN-Netzwerk haben, damit diese Anzeige nicht von der örtlichen Internetverbindung abhĂ€ngig ist. +Diese, ich sage einmal “Offline-Live-Ergebnisse” könnten dann auch auf dem GelĂ€nde als Info-Anzeigen genutzt werden. + +Die Aufgabe der Person am Abreiteplatz ist bei solchen Veranstaltungen wichtiger als man glauben mag, leider wird diese oft sehr unterschĂ€tzt. +Die Person am Abreiteplatz hat die Uhrzeit stĂ€ndig im Auge und achtet darauf, dass jeder Bewerb pĂŒnktlich beginnen kann. +Sie/Er sorgt dafĂŒr, dass die Teilnehmer in der richtigen Reihenfolge laut Startliste rechtzeitig an den Start gehen. +Sie/Er sollte stets den Überblick behalten, dass die nĂ€chsten Teilnehmer in Vorbereitung sind. +Andernfalls hat dieser dies gleich dem Richterturm zu melden. +Das ist sehr wichtig fĂŒr einen gelungenen, flĂŒssigen und vor allem stressfreien Ablauf der Veranstaltung. + +Noch eine Reiterin lĂ€uft ganz aufgeregt herum, sieht mich, lĂ€uft zu mir und fragt mich +“Hast du die Aufgabe?” +Ich: “Welche brauchst du denn?” +Reiterin: “Ich weis nicht, 2ter Bewerb” +Ich: “Das ist die Reiternadel-PrĂŒfung” +Reiterin: “Ja kann sein. Ich hatte Sie auswendig gelernt, aber jetzt ist weis ich sie nicht mehr. Habt Ihr Ansager?” +Ich: “Moment, da muss ich nachfragen” + +Kurzer Anruf an die Schreiberin vom Dressur-Richterturm. +Ich: "Anna, weißt du, haben wir Ansager?” +Anna: “Ich kann sicher einen Ansager auftreiben, um wem geht es denn?” + +Ich zur Reiterin “Wer bist du denn?” +Reiterin “Olivia” + +Ich zu Anna am Telefon: “Olivia im Reiternadel Bewerb” +Anna: “Habe ich mir notiert” +Ich: “Super, danke” + +“Also, Olivia, es gibt die Möglichkeit einer Ansage, kann nur sein, dass wir € 2,- bis € 5,- pro Ansage Trinkgeld abkassieren. Ist das Okay fĂŒr dich” +Olivia: “Ja ist mir egal, Hauptsache ich bekomme UnterstĂŒtzung” + +“Servus Mo, du altes Haus\! Wie geht\`s deinen Kindern?” +Ich: “Heidi, schön dich zu sehen. Was machst du hier?” +Heidi: “Michi hat mich gebeten den Abreiteplatz zu machen” +Ich: “Super Idee vom Michi. Danke der Nachfrage, meinen Kinder geht\`s gut.” + +Das ist gut das Heidi den Abreiteplatz macht, Sie kennt die Reiter und den Ablauf, schließlich war Sie schon öfters als FEI-Steward bei grĂ¶ĂŸeren Veranstaltungen eingesetzt, die macht das. + +8:00 Uhr + +Die ersten Bewerbe haben endlich begonnen und bis jetzt lĂ€uft alles ganz nach Plan. + +“Hallo, kann ich schon zahlen?” +Ich: “Guten Morgen, natĂŒrlich darfst du zahlen. Wem möchtest du denn abrechnen?” +Mutter einer Teilnehmerin: “ Ich zahl die Starts von Theresa L.” +Ich: “Einen Moment bitte. Ja, Sie startet den Reiternadel-Bewerb. Das Pferd ist auch mit Nadine M. am Start in der L. Zahlst du gleich alles?” +Mutter: “Nein, ich zahle nur den start meiner Tochter, das ist das Pferd von Ihrer Trainerin” +Ich: “Okay, dann bekomme ich von Dir € 15,-” + +Bei C-Neu Turnieren zahlen die Teilnehmer in den unteren Klassen kein Sportförderungs-Euro und auch kein Nenngeld. +Das Programm muss in der Lage sein , Reiter- und aber auch Pferde- bezogen Abrechnungen durchzufĂŒhren. +Es kommt auch öfter vor, dass Berufsreiter, Trainer, ZĂŒchter oder sonstige Einzel- und/oder Sammel- oder Rechnungen fĂŒr Sponsoren, Firmen oder sonstige Personen bezogen benötigen. + +Manche Veranstalter sind so freundlich und stellen mir als Meldestelle "Wechselgeld" zur VerfĂŒgung, welches ich natĂŒrlich auch in das Ein/Ausgabenbuch vermerken muss. + +8:35 Uhr + +“Hier, die soll ich dir geben” +Ein MĂ€dchen schnellen Schrittes mit den Dressur-Protokollen in HĂ€nden steckt mir diese entgegen. +Ich: “Danke\!” + +Kontrolle, ob die Ergebnisse korrekt in das System ĂŒbertragen worden sind, Ergebnislisten ausdrucken und auf die RĂŒckseite der Handschriftlichen Dressur-Protokolle anheften. +Fertig, ab zur Ablage vor meiner Meldestelle, damit sich die Teilnehmer Ihre Protokolle abholen können. + +Kurze Zeit spĂ€ter lĂ€uft Anna an der Meldestelle vobei. + +Ich: “Und, Anna, hattest du Probleme?” +Anna: “Nein, war genau so, wie du mir gesagt hast und danke fĂŒr die Pickerln. Das hat mir viel Schreibarbeit abgenommen.” +Ich: “Sehr gut, da lege ich dir immer die nĂ€chsten Protokolle hin, falls ich einmal nicht da sein sollte, okay?” +Anna: “Ja, passt. Am Nachmittag kommt eine anderer, statt mir zu schreiben.” +Ich: “Okay, erklĂ€rst du Ihr dann gleich, wie es geht?” +Anna: “Ja, kann ich machen” + +Die Spring-Ergebnisse kontrollieren. +Ich gehe selber mal zum Richterturm und sehe nach dem Rechten. + +Spring-Richterturm + +Ich: “Und Franz, hast du alles unter kontrolle?” +Franz: “Hey Mo, hast Ausgang bekommen?” +Ich: “Ein bischen die Beine vertreten. Netter Parcours” +Ein Blick zur Schreiberin Sissi +Ich: “Ah, ihr habt ja schon eine Parcours-Skizze von Markus. Da schau Sissi, hier trĂ€gst du dann bitte die Erlaubte-Zeit ein, okay? Das System berechnet automatisch die Idealzeit.” +Sissi: “Aja okay” +Ich: “Und damit Ihr schnell Siegerehrungen machen könnt, brauchst du nur nach hier "VorlĂ€ufiges-Ergebnis" klicken, dann hast du eine schönere, grĂ¶ĂŸere Anzeige ĂŒber die Ergebnisse” +Sissi: “Wieso, vorlĂ€ufig?" +Ich: “Die Ergebnisse werden noch einmal kontrolliert und erst wenn alles richtig ist wird daraus das Endergebnis" + +Teilnehmer haben bis zu einer halben Stunde nach Beendigung des Bewerbes das Recht, Einspruch zu erheben. +Was zum GlĂŒck nicht oft vorkommt. +Auf diesem Turnier ist keine elektronische Zeitnehmung im Einsatz, der/die Richter stoppen die Zeit mit einer Hand-Stoppuhr. Wenn dies der Fall ist, wird die gestoppte Zeit des Starters auf maximal zehntel Sekunden Genauigkeit fĂŒr das Ergebnis ĂŒbernommen. + +Ich: “Servus, Markus. Hast du auch fĂŒr mich deine Parcours-Skizzen?” +Markus: “Ja, gleich muss nur noch den letzten Bewerb zeichnen. Ich komm dann bei die vorbei” + +Wie immer etwas chaotisch mit ihm, aber er baut schöne, anspruchsvolle Parcours. + +Ich setzte meinen Rundgang fort, auch Heidi hat den Abreiteplatz voll im griff. +Markus pusht seine SchĂŒler, da lĂ€utet das Meldestellen-Handy, Franz ruft mich an + +Ich: “Ja?” +Franz: "Pferde Passkontrolle, machen wir Bewerb 7” +Ich: “Okay” + +Damit ist mein Rundgang auch schon wieder vorbei. +In der Meldestelle angelangt schreibe ich gleich auf die Web-Seite + +“Pferde-Passkontrolle Bewer 7” + +Auch auf der Startliste, welche im Internet veröffentlicht ist, vermerke ich den Zusatz rot, damit diese Änderung auffĂ€llt. +Einen Zettel drucke ich aus, um es auf der Anschlagtafel auszuhĂ€ngen. +Der Turniersprecher hat diese Neuerung ebenfalls verkĂŒndet und wird dies noch etliche Male bis zum Beginn des Bewerbes 7 wiederholen. + +Kiste vorbereiten fĂŒr die PferdepĂ€sse und eine Startliste zum Abstreichen. +Wenn alle PferdepĂ€sse eingelangt sind, wird die/der Turnier-Tierarzt verstĂ€ndige, dieser kontrolliert die eingetragenen Pflichtimpfungen der Pferde. +Nicht ordnungsgemĂ€ĂŸ eingetragene Impfungen werden laut ÖTO geahndet. +Ist ein Pferdepass nicht rechtzeitig vor Beginn des Bewerbes eingetroffen, ist dieser Teilnehmer nicht startberechtigt. +Im Endeffekt entscheidet der Turnier- Beauftragte Richter, was geschehen wird. + +Markus: “Da hast” + +Markus ĂŒberreicht mir einen USB-Stick mit seinen Parcours-Skizzen + +Ich: “Hast das in PDF-format ĂŒbertragen?” +Markus: “glaub schon” + +Reiterin kommt vorbei. + +“Gibt\`s schon Protokolle?” +Ich: “Ja, da vorne” +Reiterin: “Die Ergebnisse?” +Ich: “Hinten dran und im Internet” +Reiterin: “Kannst du das hier erkennen, was das heißen soll?” + +Und zeigt mir das Protokoll mit den Hieroglyphen + +Ich: “Hmm 
 könnte nicht taktrein heißen” +Reiterin: “Das könnte sein, ja danke” + +Ring ring 
 +Das Meldestellen-Handy lĂ€utet + +“Sissi, was gibt\`s?” +Sissi: “Das Programm schreibt mir gerade, dass es keine Verbindung zur Meldestelle mehr hat, was soll ich tun?” +Ich: “Kein Problem, schreib einfach weiter, ich werde die Verbindung gleich kontrollieren.” + +Na super, bis jetzt lief alles so gut und jetzt das noch. +Okay, wo könnte das Problem liegen? +Bei mir in der Meldestelle ist alles angeschlossen und hat Strom. +Kontrollieren wir einmal das erste Funkie, Kabel ist in Ordnung und Strom ist auch vorhanden. +EmpfĂ€nger-Funkie, ah das hat keinen Strom. +Warum? Angeschlossen ist alles. Ah, da haben wir den ÜbeltĂ€ter, das POE-Netzteil ist ĂŒberhitzt, weil die Sonne direkt darauf scheint, hat der Überhitzungsschutz abgeschaltet. +Zum GlĂŒck habe ich noch ein Ersatz-Netzteil mit, gleich wechseln und irgendwie abdecken, damit es von der Sonneneinstrahlung besser geschĂŒtzt ist. +Kontrollgang zur Sissi, ob Sie wieder eine Verbindung hat und auch gleich bei Ihr darauf achten, dass das Netzteil vom Laptop und natĂŒrlich auch den Laptop selbst von der Sonne etwas schĂŒtzen. + +ZurĂŒck zur Meldestelle, sind schon ein paar Leute angestellt. + +“Bin schon da, was kann ich antun?” +Teilnehmer: “Wir wollen zahlen\!” +Ich: “Warum sagt Ihr das nicht gleich? DafĂŒr lasse ich doch alles liegen und stehen.” + +WĂ€hrend ich unterwegs war, um die Verbindung wiederherzustellen, hatten die wartenden Teilnehmer fast die gesamten SĂŒĂŸigkeiten, die ich als kleine VersĂŒĂŸung zur Meldestelle aufgestellt hatte, vernascht. +Naja, eine Packung habe ich noch in Reserve. + +Kaum hatte ich diese Traube an Teilnehmer abgefertigt, ruft mich Anna an. + +“Ja, was gibt\`s” +Anna: “Die Reiter brauchen fĂŒr die Aufgabe lĂ€nger als die 3 min. 30 sek. Dadurch sind wir im Verzug” +Ich: “Okay, lass mich kurz nachsehen. Wir sind ca. 10 min. im Verzug. Stimmt das in etwa?” +Anna: “Ja, das kommt hin.” +Ich: “Das heist, wenn der Bewerb vorbei ist und alle weiteren Starter jetzt wie lange brauchen?” +Anna: “Gute Minute bis 1,5 Minuten brauchen die lĂ€nger” +Ich: “Das heist wir haben abzĂŒglich der Mini-Pause dazwischen ca. 25 Minuten verspĂ€tung fĂŒr den nĂ€chsten Bewerb. Danach haben wir eh die kleine Mittagspause. Dadurch haben wir die VerspĂ€tung wieder kompensiert. Okay, wir machen gleich ohne Pause mit dem nĂ€chsten Bewerb weiter und die Siegerehrung findet ohne Pferd in der Gastro statt, okay. Sagst du das dem Sprecher, das er dass gleich so verkĂŒnden soll. Der nĂ€chste Bewerb ca. 20 Minuten verspĂ€tung soll er sagen, okay? +Anna: “Okay, ohne Pause ca. 20 Minuten nĂ€chster Bewerb VerspĂ€tung, Siegerehrung ohne Pferd in der Gastro, passt sage ich Ihm” + +Auch das noch. Eine Dressuraufgabe, die lĂ€nger dauert als auf dieser angeschlagen steht und noch dazu in einem Bewerb, wo wir mehr Starter haben. +Gleich diese neue Startzeit fĂŒr den nĂ€chsten Bewerb im Internet deutlich machen und dem Abreiteplatz (Heidi) bescheid geben. +Da wird es sicher zumindest einen Dressur-Reiter geben die/der sich aufregen wird. + diff --git a/docs/02_Domain/03_Analysis/Use_Cases_Draft.md b/docs/02_Domain/03_Analysis/Use_Cases_Draft.md new file mode 100644 index 00000000..87709a14 --- /dev/null +++ b/docs/02_Domain/03_Analysis/Use_Cases_Draft.md @@ -0,0 +1,117 @@ +--- +type: ADR +status: DRAFT +owner: Lead Architect +--- +# Use Cases Draft - Phase 1 (Core Domain) + +* **Status:** Draft +* **Fokus:** High-Level ProzessflĂŒsse und Systemgrenzen + +--- + +## Cluster 1: Turnier-Initialisierung & Datenbasis + +### UC-01: Turnier-Stammdaten importieren +* **Akteur:** Meldestellen-Leiter +* **Auslöser:** Vorbereitung eines neuen Turniers oder Update am Turniermorgen. +* **Vorbedingung:** `zns.zip` (oder Ă€quivalente OEPS-Daten) liegt vor. +* **Ablauf:** + 1. System liest die DatensĂ€tze fĂŒr Pferde, Reiter, Vereine und FunktionĂ€re. + 2. System aktualisiert die lokale Datenbank (Insert/Update). + 3. System markiert DatensĂ€tze mit Sperrvermerken oder fehlenden Lizenzen. +* **Nachbedingung:** Die lokale Datenbank ist die "Single Source of Truth" fĂŒr Validierungen. + +### UC-02: Turnier-Konfiguration anlegen +* **Akteur:** Meldestellen-Leiter +* **Auslöser:** Erstellung eines neuen Events. +* **Vorbedingung:** Ausschreibung liegt vor. +* **Ablauf:** + 1. Akteur definiert Stammdaten (Ort, Datum, Veranstalter). + 2. Akteur legt Bewerbe an (Nummer, Klasse, Richtverfahren). + 3. Akteur definiert GebĂŒhren (Nenngeld, Startgeld, Boxenpreise). +* **Nachbedingung:** Das TurniergerĂŒst steht bereit fĂŒr Nennungen. + +--- + +## Cluster 2: Nennungs-Management (Pre-Competition) + +### UC-03: Nennung erfassen & validieren +* **Akteur:** Meldestellen-Mitarbeiter +* **Auslöser:** Import von Online-Nennungen oder manuelle Eingabe. +* **Ablauf:** + 1. System prĂŒft Existenz von Reiter und Pferd (via Satznummer). + 2. **Validierung:** + * Ist die Startkarte bezahlt? + * Ist die Lizenz ausreichend fĂŒr die Klasse? + * Liegt eine Sperre vor? + * Ist das Pferd geimpft/registriert? + 3. Bei Validierungsfehler: System zeigt Warnung, erlaubt aber "Override" durch Akteur (z.B. "Zahlung erfolgt"). + 4. System verknĂŒpft Paar mit Bewerb. +* **Nachbedingung:** Das Paar ist auf der "Nennliste" (noch nicht Starterliste). + +### UC-04: Pferd/Reiter tauschen +* **Akteur:** Meldestellen-Mitarbeiter +* **Auslöser:** Reiter fĂ€llt aus oder Pferd ist lahm. +* **Ablauf:** + 1. Akteur wĂ€hlt bestehende Nennung. + 2. Akteur tauscht Reiter ODER Pferd aus. + 3. System fĂŒhrt Validierung (UC-03) fĂŒr die neue Kombination durch. + 4. System protokolliert den Tausch (relevant fĂŒr T-Satz im Export). +* **Nachbedingung:** Nennung ist aktualisiert, Historie ist gewahrt. + +--- + +## Cluster 3: DurchfĂŒhrung & Sport (Competition) + +### UC-05: Startliste erstellen +* **Akteur:** Meldestellen-Leiter +* **Auslöser:** Nennschluss fĂŒr einen Bewerb ist erreicht. +* **Ablauf:** + 1. Akteur definiert Startreihenfolge (z.B. "Alphabetisch", "Gelost", "Nach Lizenz"). + 2. System generiert die Reihenfolge. + 3. System weist Kopfnummern zu (falls noch nicht geschehen). + 4. System teilt bei Bedarf in Abteilungen (siehe US-005). +* **Nachbedingung:** Die Startliste ist fixiert und kann gedruckt/publiziert werden. + +### UC-06: Ergebnis erfassen +* **Akteur:** Richter / Schreiber / Zeitnehmung +* **Auslöser:** Ein Ritt ist beendet. +* **Ablauf:** + 1. Akteur wĂ€hlt Starter. + 2. Akteur gibt Rohdaten ein (Zeit, Fehlerpunkte, Wertnote). + 3. System berechnet sofort den Score und den vorlĂ€ufigen Rang. + 4. System prĂŒft auf SpezialfĂ€lle (Ausschluss, Aufgabe). +* **Nachbedingung:** Ergebnis ist gespeichert, Live-Ranking ist aktualisiert. + +### UC-07: Bewerb abschließen +* **Akteur:** Meldestellen-Leiter / Hauptrichter +* **Auslöser:** Letzter Reiter ist fertig, Einspruchsfrist abgelaufen. +* **Ablauf:** + 1. System finalisiert die Rangierung (inkl. Ex-Aequo Regeln). + 2. System berechnet Geldpreise gemĂ€ĂŸ Ausschreibung und Teilnehmerzahl. + 3. System sperrt den Bewerb fĂŒr Änderungen. +* **Nachbedingung:** Ergebnisse sind "amtlich", Geldpreise sind den Konten gutgeschrieben. + +--- + +## Cluster 4: Abschluss & Finanzen + +### UC-08: Konto abrechnen (Kassieren) +* **Akteur:** Kassen-Mitarbeiter +* **Auslöser:** Teilnehmer will abreisen/bezahlen. +* **Ablauf:** + 1. System aggregiert alle Kosten (Nenngelder, Boxen, GebĂŒhren) pro "Verantwortlicher Person". + 2. System zieht gewonnene Geldpreise ab. + 3. System erstellt Saldo. + 4. Akteur verbucht Zahlungseingang. +* **Nachbedingung:** Konto ist ausgeglichen, "Horse Pass" kann ausgegeben werden. + +### UC-09: OEPS-Export durchfĂŒhren +* **Akteur:** Meldestellen-Leiter +* **Auslöser:** Turnierende. +* **Ablauf:** + 1. System prĂŒft DatenintegritĂ€t (Alle Pflichtfelder fĂŒr Export vorhanden?). + 2. System generiert `XXXXX.ERG` Datei gemĂ€ĂŸ Spezifikation V2.4. + 3. System erstellt Protokoll ĂŒber eventuelle Warnungen/Abweichungen. +* **Nachbedingung:** Export-Datei liegt bereit zur Übermittlung. diff --git a/docs/02_Domain/03_Analysis/User_Stories_Draft.md b/docs/02_Domain/03_Analysis/User_Stories_Draft.md new file mode 100644 index 00000000..767aaac3 --- /dev/null +++ b/docs/02_Domain/03_Analysis/User_Stories_Draft.md @@ -0,0 +1,114 @@ +--- +type: ADR +status: DRAFT +owner: Lead Architect +--- +# User Stories Draft - Phase 1 (Core Domain) + +* **Status:** Draft +* **Fokus:** Nationale Turniere (OEPS), Offline-Betrieb, Basis-Verwaltung + +--- + +## Epic 1: Stammdaten & Offline-Vorbereitung + +### US-001: Import der Verbands-Stammdaten (ZNS) +**Als** Meldestellen-Leiter +**möchte ich** die offizielle `zns.zip` Datei (Pferde, Reiter, Vereine, Richter) in das System importieren, +**damit** ich auch ohne Internetverbindung Zugriff auf alle validen Lizenz- und Pferdedaten habe. + +* **Akzeptanzkriterien:** + * System akzeptiert `zns.zip` oder entpackte `.dat` Dateien (Codepage 850). + * Importiert `PFERDE01.dat` (inkl. Mapping der 10-stelligen Satznummer). + * Importiert `LIZENZ01.dat` (inkl. Startkarten-Status und Sperrvermerke). + * Der Import ist performant genug, um am Turniermorgen aktualisiert zu werden (< 5 Min). + * Fehlerhafte DatensĂ€tze werden protokolliert, brechen den Import aber nicht ab. + +### US-002: Intelligente Akteur-Suche +**Als** Meldestellen-Mitarbeiter +**möchte ich** Reiter und Pferde ĂŒber eine fehlertolerante Suche finden (Name, Kopfnummer, Lizenznummer), +**damit** ich Nennungen schnell erfassen kann, auch wenn der Reiter seine genaue Nummer nicht weiß. + +* **Akzeptanzkriterien:** + * Suche nach Pferdenamen (TeilĂŒbereinstimmung). + * Suche nach Kopfnummer (z.B. "A123"). + * Anzeige von Warnhinweisen direkt im Suchergebnis (z.B. "Sperrliste", "Keine Startkarte"). + * Unterscheidung bei Namensgleichheit durch Anzeige von Verein/Jahrgang/Abstammung. + +--- + +## Epic 2: Nennung & Check-in + +### US-003: Validierung der Startberechtigung (Startkarte) +**Als** Meldestellen-Leiter +**möchte ich**, dass das System mich warnt, wenn ein Reiter fĂŒr einen Bewerb nennt, aber keine aktive Startkarte (JahresgebĂŒhr) hat, +**damit** ich ihn zur Nachzahlung auffordern kann. + +* **Akzeptanzkriterien:** + * PrĂŒfung des Flags `STARTKARTE` aus den Stammdaten. + * PrĂŒfung der Lizenzklasse (z.B. darf "R1" nicht in Klasse S starten). + * **Wichtig:** Das System darf die Nennung *nicht* blockieren (Soft-Validation), sondern muss einen "Override" ermöglichen (z.B. "Zahlung vor Ort erfolgt"). + * Visuelle Hervorhebung in der Starterliste (z.B. roter Status). + +### US-004: Manuelle Nachnennung vor Ort +**Als** Meldestellen-Mitarbeiter +**möchte ich** ein Pferd-Reiter-Paar kurzfristig zu einem Bewerb hinzufĂŒgen, +**damit** Teilnehmer, die die Online-Nennfrist verpasst haben, gegen GebĂŒhr noch starten können. + +* **Akzeptanzkriterien:** + * Auswahl von Bewerb, Reiter und Pferd. + * Automatische Berechnung der erhöhten NenngebĂŒhr (NachnenngebĂŒhr). + * Vergabe einer Startnummer (fortlaufend oder manuell). + * Eintrag in die `KKARTEI` (Nennliste) und `BBEWERBE` (Starterliste). + +--- + +## Epic 3: Bewerbs-Abwicklung + +### US-005: Verwaltung von Abteilungen +**Als** Meldestellen-Leiter +**möchte ich** einen Bewerb mit vielen Startern in mehrere Abteilungen (z.B. R1-Reiter vs. R2-Reiter) unterteilen, +**damit** ich getrennte Ergebnislisten und Platzierungen erstellen kann, wie es die ÖTO verlangt. + +* **Akzeptanzkriterien:** + * Ein Bewerb kann in n Abteilungen gesplittet werden. + * Starter können per Drag&Drop oder Regel (z.B. "Alle R1 in Abt. 1") zugewiesen werden. + * Jede Abteilung hat eine eigene Platzierung, aber sie teilen sich die gleichen PrĂŒfungsparameter (Parcours). + * Export berĂŒcksichtigt das Feld `ABTEILUNG` im B-Satz. + +### US-006: Ergebniserfassung & Platzierung +**Als** Richter oder Schreiber +**möchte ich** Ergebnisse (Zeit, Fehler, Wertnote) fĂŒr einen Starter eingeben, +**damit** die Rangierung automatisch berechnet wird. + +* **Akzeptanzkriterien:** + * Eingabemaske optimiert fĂŒr schnelle Nummernblock-Eingabe. + * Automatische Berechnung der Rangfolge basierend auf dem Regelwerk (Fehler/Zeit vs. Wertnote). + * Handling von SpezialfĂ€llen: Ausschluss (EL), Aufgabe (RET), Disqualifikation (DQ), Nicht angetreten (DNS). + * Sofortige Aktualisierung der "Live-Ergebnisse". + +--- + +## Epic 4: Abschluss & Export + +### US-007: OEPS-Konformer Ergebnis-Export +**Als** Meldestellen-Leiter +**möchte ich** die Ergebnisse des Turniers in das definierte Format (ASCII, Codepage 850) exportieren, +**damit** ich meiner Meldepflicht gegenĂŒber dem Verband nachkommen kann. + +* **Akzeptanzkriterien:** + * Erstellung der `XXXXX.ERG` Datei. + * Strikte Einhaltung der Spaltenbreiten und Formate (siehe Legacy Spec Analyse). + * Validierung vor Export: Warnung bei fehlenden Satznummern oder ungĂŒltigen Codes. + * Korrekte Zuordnung der Nation (Gast vs. InlĂ€nder). + +### US-008: Kassenabschluss & Abrechnung +**Als** Veranstalter +**möchte ich** eine Liste aller offenen Posten (Nenngelder, Boxen, NachnenngebĂŒhren) pro Reiter/Verein sehen, +**damit** ich vor der Ausgabe der PferdepĂ€sse kassieren kann. + +* **Akzeptanzkriterien:** + * Aggregierte Ansicht pro "Verantwortlicher Person" (Zahler). + * Auflistung aller Posten (Nennung, Startgeld, GebĂŒhren). + * Verrechnung von gewonnenen Geldpreisen (Gutschrift). + * Druckfunktion fĂŒr Rechnung/Quittung. diff --git a/docs/02_Domain/Events/Neumarkt2026/26128.erg b/docs/02_Domain/Events/Neumarkt2026/26128.erg new file mode 100644 index 00000000..db0e71ab --- /dev/null +++ b/docs/02_Domain/Events/Neumarkt2026/26128.erg @@ -0,0 +1,123 @@ +A26128CSN-C-NEU CSNP-C-NEU NEUM2026042520260425CSN-C-Neu CSNP-C_Neu 2.2PSO v1.07 +B010Stilspringprüfung - CSNP-C_N006000000001 +C010001307002129000000000000000000000000000000000000021771000000 +D001PG47Paddy's Nikita 170107Remplbauer Selina 00080000000 000000AUT* +D002PK06H-S Button 196040Gillinger Marlene 00067000000 000000AUT* +D003P824Pit 3 184759Krenn Eva 00055000000 000000AUT* +D004P814Balu 6 193244Remplbauer Sophia 00000000000 000000AUT +D004P901Daneder's Blitz 195501Weidinger Janina 00000000000 000000AUT +D004PB70Daneder's Caramello 163545Montgomery Helena 00000000000 000000AUT +B021Einlaufspringprüfung - CSN-C-Ne008000000002 +C021001307002129000000000000000000000000000000000000021771000000 +D0001781Ritual Do Vizo 126532Layr Bianca 00000000000 000000AUT* +D000P816Aldensfarm Breaking Dawn 159405Starzengruber Marie-Theres 00000000000 000000AUT* +D000P901Daneder's Blitz 195501Weidinger Janina 00000000000 000000AUT* +D000PB70Daneder's Caramello 163545Montgomery Helena 00000000000 000000AUT* +D000PE14SD Antonette 929451Mayrhofer Simon 00000000000 000000AUT* +D000PG47Paddy's Nikita 170107Remplbauer Selina 00000000000 000000AUT* +D000P824Pit 3 184759Krenn Eva 00003000000 000000AUT +D000P814Balu 6 193244Remplbauer Sophia 00000000000 000000AUT +B022Einlaufspringprüfung - CSN-C-Ne003000000002 +C022001307002129000000000000000000000000000000000000021771000000 +D000AR70Chocolate Kiss 2 147265Vanova Nina 00000000000 000000AUT*10258795 +D000P561Ginger Bread Girl 153601Winter Maja Sophie 00000000000 000000AUT* +D997Z001Wildberry Gold RPZ 168660Zechmeister-Paster Diana A00000000000 000000AUT +B030Stilspringprüfung - CSNP-C_N006000000003 +C030001307002129000000000000000000000000000000000000021771000000 +D001PA53Rathcline Star 178474Schmidmayr Nena Sophie 00072000000 000000AUT* +D002P152Verena 3 170454Krenn Miriam 00070000000 000000AUT* +D003P816Aldensfarm Breaking Dawn 159405Starzengruber Marie-Theres 00068000000 000000AUT* +D004P561Ginger Bread Girl 153601Winter Maja Sophie 00067000000 000000AUT* +D997PE14SD Antonette 929451Mayrhofer Simon A00000000000 000000AUT +D997PK06H-S Button 196040Gillinger Marlene A00000000000 000000AUT +B041Einlaufspringprüfung - CSNP-C_N006000000004 +C041001307002129000000000000000000000000000000000000021771000000 +D0002M80Handsome 186927Lengauer Jelena 00000000000 000000AUT* 106KB09 +D000AN19Exklusiv EM 187665Mück Hannah 00000000000 000000AUT* +D0001781Ritual Do Vizo 126532Layr Bianca 00040000000 000000AUT +D0004Y59Legolas 196 925183Schreiber Tamina 00047000000 000000GER +D000AB83HB Vijola 920327Reisinger Marlene 00056000000 000000AUT +D0003E99Quinet 906586Kapeller Emilia 00000000000 000000AUT +B042Einlaufspringprüfung - CSNP-C_N007000000004 +C042001307002129000000000000000000000000000000000000021771000000 +D0003K69Lillet 18 150620Reitetschläger Lena 00000000000 000000AUT* +D0005789Furiosa de la Bryere CE 140156Ehrentraut Carina 00000000000 000000AUT* +D000A099Quintessa 2 609548Aichinger Bianca 00000000000 000000AUT* +D0003M58Samantha 25 609771Karl Reinhard 00040000000 000000AUT +D000H606Moondancer 070156Alberer Manuela 00040000000 000000AUT +D000Z001Wildberry Gold RPZ 168660Zechmeister-Paster Diana 00092500000 000000AUT +D000AR70Chocolate Kiss 2 147265Vanova Nina 00129000000 000000AUT 10258795 +B050Stilspringprüfung - CSNP-C_N003000000005 +C050001307002129000000000000000000000000000000000000021771000000 +D001P152Verena 3 170454Krenn Miriam 00074000000 000000AUT* +D002P985Taffy 2 193430Schartmüller Sarah 00072000000 000000AUT* +D003PA53Rathcline Star 906580Egger Julia 00065000000 000000AUT* +B061Stilspringprüfung - CSNP-C_N009000000006 +C061001307002129000000000000000000000000000000000000021771000000 +D0012B41Guccini 922710Simlinger Marlies 00075000000 000000AUT* +D002AN19Exklusiv EM 187665Mück Hannah 00072000000 000000AUT* +D0033E99Quinet 906586Kapeller Emilia 00071000000 000000AUT* +D0042M80Handsome 186927Lengauer Jelena 00070000000 000000AUT* 106KB09 +D004AF41Cäsar 55 916541Dugandzic Sarah 00070000000 000000AUT* +D006PA53Rathcline Star 906580Egger Julia 00068000000 000000AUT +D0074Y59Legolas 196 925183Schreiber Tamina 00062000000 000000GER +D0083785Coeur 17 145963Obermüller Hannah 00061000000 000000AUT +D009AB83HB Vijola 920327Reisinger Marlene 00057000000 000000AUT +B062Stilspringprüfung - CSNP-C_N007000000006 +C062001307002129000000000000000000000000000000000000021771000000 +D001A099Quintessa 2 609548Aichinger Bianca 00082000000 000000AUT* +D0025789Furiosa de la Bryere CE 140156Ehrentraut Carina 00072000000 000000AUT* +D0033K69Lillet 18 150620Reitetschläger Lena 00067000000 000000AUT* +D004KSS1Charity Coke 053749Eichler Eva 00065000000 000000AUT* +D0053M58Samantha 25 609771Karl Reinhard 00060000000 000000AUT +D005H606Moondancer 070156Alberer Manuela 00060000000 000000AUT +D9971A11Gradan 102783Steyrer Anna A00000000000 000000AUT +B070Stilspringprüfung - CSNP-C_N002000000007 +C070001307002129000000000000000000000000000000000000021771000000 +D001Y001Bella Graziella 144315Gaugl Laura 00075000000 000000AUT* +D002P985Taffy 2 193430Schartmüller Sarah 00000000000 000000AUT +B080Springreiterbewerb - CSNP-C_N003000000008 +C080001307002129000000000000000000000000000000000000021771000000 +D0013785Coeur 17 145963Obermüller Hannah 00080000000 000000AUT* +D0022M80Handsome 186927Lengauer Jelena 00072000000 000000AUT* 106KB09 +D9973E99Quinet 178474Schmidmayr Nena Sophie A00000000000 000000AUT +B091Standardspringprüfung - CSNP-C_N005000000009 +C091001307002129000000000000000000000000000000000000021771000000 +D0012062Grover 157407Pröll Leonie 00000005416 000000AUT* +D0022B41Guccini 160813Grubmüller Lea 00000005463 000000AUT* +D0031317Quality's Finest 612295Stroblmair Victoria 00000005492 000000AUT* +D0041A11Gradan 102783Steyrer Anna 00000005858 000000AUT* +D005KSS1Charity Coke 053749Eichler Eva 00040006428 000000AUT +B092Standardspringprüfung - CSNP-C_N007000000009 +C092001307002129000000000000000000000000000000000000021771000000 +D001A024D Day 075374Ambros Susanne 00000005940 000000AUT*10071068 108EH50 +D0021G88Hamira 3 074007Beißmann Andreas 00000005991 000000AUT* +D0032G77S Mirrallas 605835Ellmer Kassandra 00000006298 000000AUT* +D0043966Capitaine 601366Madlmayr Carina 00040005862 000000AUT +D0051942Obora's Agnetha 601300Hofer Michaela 00040005966 000000AUT +D006Y001Bella Graziella 144315Gaugl Laura 00080005012 000000AUT +D9972785Herr Frodo 144315Gaugl Laura A00000000000 000000AUT +B100Springpferdeprüfung - CSN-C-Ne000000000010 +C100001307002129000000000000000000000000000000000000021771000000 +B110Stilspringprüfung - CSN-C-Ne002000000011 +C110001307002129000000000000000000000000000000000000021771000000 +D0012062Grover 157407Pröll Leonie 00085000000 000000AUT* +D0021317Quality's Finest 612295Stroblmair Victoria 00080000000 000000AUT* +B121Standardspringprüfung - CSN-C-Ne002000000012 +C121001307002129000000000000000000000000000000000000021771000000 +D0012062Grover 157407Pröll Leonie 00040005651 000000AUT* +D0022B41Guccini 160813Grubmüller Lea 00080005774 000000AUT +B122Standardspringprüfung - CSN-C-Ne004000000012 +C122001307002129000000000000000000000000000000000000021771000000 +D001AS94Landliebe 3 162776Höllmüller Anna 00000005557 000000AUT*10294537 +D0022G77S Mirrallas 605835Ellmer Kassandra 00000006212 000000AUT* +D0031942Obora's Agnetha 601300Hofer Michaela 00000006723 000000AUT* +D004A024D Day 075374Ambros Susanne 00040005943 000000AUT 10071068 108EH50 +B130Stilspringprüfung - CSN-C-Ne001000000013 +C130001307002129000000000000000000000000000000000000021771000000 +D0014258Casino East 601300Hofer Michaela 00075000000 000000AUT* +B140Standardspringprüfung - CSN-C-Ne003000000014 +C140001307002129000000000000000000000000000000000000021771000000 +D0012010Leonidas van de Zuuthoeve Z 145960Fischerlehner Leonie 00000005368 000000AUT* +D002AS94Landliebe 3 162776Höllmüller Anna 00000005745 000000AUT*10294537 +D0034258Casino East 601300Hofer Michaela 00000006261 000000AUT* diff --git a/docs/02_Domain/Events/Neumarkt2026/26128.md b/docs/02_Domain/Events/Neumarkt2026/26128.md new file mode 100644 index 00000000..5750811b --- /dev/null +++ b/docs/02_Domain/Events/Neumarkt2026/26128.md @@ -0,0 +1,71 @@ +# CSN-C NEU / CSNP-C NEU NEUMARKT/M. + +**Turnier-Nr.: 26128** | **Datum: 25. April 2026** + +## Allgemeine Informationen + +* **Veranstalter:** Union Reit- u. Fahrverein Neumarkt/M. (6-009) +* **Ort:** Reitanlage Stroblmair, 4212 Neumarkt +* **Kontakt:** Ursula Stroblmair, Brandstetterweg 2, 4212 Neumarkt + * **Tel.:** 0664 1832381 + * **E-Mail:** reit-stall@gmx.at +* **Nennungsschluss:** 24.04.2026, 19:00 Uhr +* **Online-Nennung:** Ab Mittwoch, 22.04. + auf [www.ihremeldestelle.at](http://www.ihremeldestelle.at) +* **Meldestelle:** Geöffnet ab 24.04., 17:00 Uhr (Tel: +43 681 10769120) + +## Technische Details + +* **Austragungsplatz:** 45 x 65 m (Sand/Vlies) +* **Vorbereitungsplatz:** 20 x 40 m Halle (Sand/Vlies) +* **Warmreiten:** Draußen (20 x 60 m Sand/Vlies) möglich +* **Boxen:** Keine Einstallung möglich + +## FunktionĂ€re + +* **Turnierleiter:** Ursula Stroblmair +* **Turnierbeauftragter:** Rudi Kreupl +* **Richter:** Rudi Kreupl, Helmut Riedler +* **Parcoursbauchef:** Kurt ReitetschlĂ€gerr +* **Tierarzt:** Dr. Sabine Ötschmaier + +--- + +## Besondere Bestimmungen + +* **Kosten:** Startgeld € 15,- pro Bewerb. Kein Nenngeld, kein Sporteuro. +* **Teilnahmebedingungen:** + * FĂŒr SpringprĂŒfungen bis 95 cm: Mitgliedschaft OEPS-Verein und Reiterpass erforderlich. + * Pferde bis 90 cm mĂŒssen **nicht** beim OEPS registriert sein. + * Pferdepass mit gĂŒltigem Impfschutz (§ 11 OTO) ist vorzulegen. + * Haftpflichtversicherung fĂŒr jedes Pferd ist Pflicht. +* **Startregelung:** + * Ein Pferd darf maximal 3x pro Tag starten. + * In Bewerben bis 95 cm darf ein Pferd mit zwei verschiedenen Reitern starten. +* **Hunde:** Am gesamten GelĂ€nde herrscht Leinenpflicht. + +--- + +## Bewerbe (Samstag, 25. April 2026 - Beginn 08:00 Uhr) + +| Nr. | Bewerb | Höhe | Richtverfahren / Abteilungen | +|:-------|:--------------------------------|:-------|:-------------------------------------------------------------| +| **1** | Pony StilspringprĂŒfung | 60 cm | RV: § 204/4 (CSNP-C) | +| **2** | EinlaufspringprĂŒfung | 60 cm | RV: § 204/4 (1. Abt: lizenzfrei / 2. Abt: mit Lizenz) | +| **3** | Pony StilspringprĂŒfung | 70 cm | RV: § 204/4 (CSNP-C) | +| **4** | EinlaufspringprĂŒfung | 70 cm | RV: § 218 (1. Abt: lizenzfrei / 2. Abt: mit Lizenz) | +| **5** | Pony StilspringprĂŒfung | 80 cm | RV: § 204/4 (CSNP-C) | +| **6** | StilspringprĂŒfung | 80 cm | RV: § 204/4 (1. Abt: lizenzfrei / 2. Abt: R1 & 5-6j. Pferde) | +| **7** | Pony StilspringprĂŒfung | 95 cm | RV: § 204/4 (CSNP-C) | +| **8** | Springreiterbewerb (lizenzfrei) | 95 cm | RV: § 204/4 (CSNP-C) | +| **9** | StandardspringprĂŒfung | 95 cm | RV: A2 (1. Abt: R1 / 2. Abt: R2 und höher) | +| **10** | SpringpferdeprĂŒfung | 105 cm | RV: § 203/3 (1. Abt: 4-jĂ€hrig / 2. Abt: 5-6-jĂ€hrig) | +| **11** | StilspringprĂŒfung | 105 cm | RV: § 204/4 (1. Abt: R1) | +| **12** | StandardspringprĂŒfung | 105 cm | RV: A2 (1. Abt: R1 / 2. Abt: R2/RS2 und höher) | +| **13** | StilspringprĂŒfung | 115 cm | RV: § 204/4 (1. Abt: R1) | +| **14** | StandardspringprĂŒfung | 115 cm | RV: A2 (1. Abt: R1 / 2. Abt: R2/RS2 und höher) | + +--- +**Haftung:** Der Veranstalter ĂŒbernimmt keine Haftung. Teilnehmer haften persönlich fĂŒr SchĂ€den gegenĂŒber +Dritten. + diff --git a/docs/02_Domain/Events/Neumarkt2026/26128.pdf b/docs/02_Domain/Events/Neumarkt2026/26128.pdf new file mode 100644 index 00000000..836592dc Binary files /dev/null and b/docs/02_Domain/Events/Neumarkt2026/26128.pdf differ diff --git a/docs/02_Domain/Events/Neumarkt2026/26129.erg b/docs/02_Domain/Events/Neumarkt2026/26129.erg new file mode 100644 index 00000000..4e355954 --- /dev/null +++ b/docs/02_Domain/Events/Neumarkt2026/26129.erg @@ -0,0 +1,96 @@ +A26129CDN-C-NEU CDNP-C_NEU NEUM2026042620260426 2.2PSO v1.07 +B010Dressurprüfung lzf CDN-C_Ne002000000001 +C010000000038705000000000000000000000000000000000000000000000000 +D001PB70Daneder's Caramello 163545Montgomery Helena 00062000000 000000AUT* +D0022892Amore 5 AUT Stadler Caroline 00060000000 000000AUT* +B020Dressurprüfung lzf CDN-C_Ne003000000002 +C020000000038705000000000000000000000000000000000000000000000000 +D0014208Sahib Silver G 195331Neuhauser Lara 00075000000 000000AUT* +D0022892Amore 5 AUT Stadler Caroline 00068000000 000000AUT* +D003PB70Daneder's Caramello 163545Montgomery Helena 00060000000 000000AUT* +B030Dressurreiterprüfung lzf CDN-C_Ne005000000003 +C030035110000000000000000000000000000000000000000000000000000000 +D001PC62Flora HP 917397Altendorfer Pia 00080000000 000000AUT* +D002Z002Abrakadabra S 107926Fürbäck Melanie 00077000000 000000AUT* +D0034Y59Legolas 196 184074Stöbich Enya 00068000000 000000AUT* +D004HKTBAcceptius FA 196261Salzinger Luisa Marie 00064000000 000000AUT +D005PA53Rathcline Star 922380Kropfreiter Ines 00062000000 000000AUT +B040Dressurreiterprüfung lzf CDN-C_Ne006000000004 +C040000000038705000000000000000000000000000000000000000000000000 +D001PC62Flora HP 917397Altendorfer Pia 00078000000 000000AUT* +D0024208Sahib Silver G 195331Neuhauser Lara 00076000000 000000AUT* +D003Z002Abrakadabra S 107926Fürbäck Melanie 00068000000 000000AUT* +D004HKTBAcceptius FA 196261Salzinger Luisa Marie 00064000000 000000AUT +D0054Y59Legolas 196 184074Stöbich Enya 00062000000 000000AUT +D006PA53Rathcline Star 922380Kropfreiter Ines 00060000000 000000AUT +B050Dressurreiterprüfung lzf CDN-C_Ne001000000005 +C050035110000000000000000000000000000000000000000000000000000000 +D001PF06Domino N AUT Stelzl Helena 00080000000 000000AUT* +B060Dressurreiterprüfung lzf CDN-C_Ne000000000006 +C060035110000000000000000000000000000000000000000000000000000000 +B070Pony Dressurprüfung A CSNP-C_N003000000007 +C070000000038705000000000000000000000000000000000000000000000000 +D001PT24Daneder's Captain 146663Steinmetz Sinah-Marie 00065000000 000000AUT* +D002P561Ginger Bread Girl 153601Winter Maja Sophie 00060000000 000000AUT* +D003PA53Rathcline Star 906592Emsenhuber Tanja 00058000000 000000AUT +B081Dressurreiterprüfung A CDN-C_Ne006000000008 +C081035110038705000000000000000000000000000000000000000000000000 +D0013888Ravasz 123156Scheiblechner Sonja 00072000000 000000AUT* +D0024307Makker 146066Gstöttenbauer Olivia 00064000000 000000AUT* +D003P561Ginger Bread Girl 153601Winter Maja Sophie 00062000000 000000AUT* +D0042083Light Blue 194297Hazoth Anna-Maria 00060000000 000000AUT* +D0053M58Samantha 25 609771Karl Reinhard 00058000000 000000AUT +D005KSS1Charity Coke 053749Eichler Eva 00058000000 000000AUT +B082Dressurreiterprüfung A CDN-C_Ne002000000008 +C082035110038705000000000000000000000000000000000000000000000000 +D001GIGIGigi D'Agostidinina 076742Klein Elisabeth 00068000000 000000AUT*10144403 +D002A590Queeny 8 612592Panzirsch Anna 00064000000 000000AUT* +B091Dressurprüfung A CDN-C_Ne007000000009 +C091035110038705000000000000000000000000000000000000000000000000 +D0013888Ravasz 123156Scheiblechner Sonja 00070000000 000000AUT* +D002AN19Exklusiv EM 187665Mück Hannah 00064000000 000000AUT* +D0032083Light Blue 194297Hazoth Anna-Maria 00062000000 000000AUT* +D0044B66Vingino's Victory 616957Kiesenhofer Sarah 00058000000 000000AUT +D0053M58Samantha 25 609771Karl Reinhard 00055000000 000000AUT +D005KSS1Charity Coke 053749Eichler Eva 00055000000 000000AUT +D0074307Makker 146066Gstöttenbauer Olivia 00053000000 000000AUT +B092Dressurprüfung A CDN-C_Ne004000000009 +C092035110038705000000000000000000000000000000000000000000000000 +D001GIGIGigi D'Agostidinina 076742Klein Elisabeth 00068000000 000000AUT*10144403 +D002AL46Superbunt 616836Lengauer Julia 00065000000 000000AUT* +D0032010Leonidas van de Zuuthoeve Z 145960Fischerlehner Leonie 00064000000 000000AUT* +D004A590Queeny 8 612592Panzirsch Anna 00058000000 000000AUT +B100Pony Dressurprüfung L CSNP-C_N001000000010 +C100035110038705000000000000000000000000000000000000000000000000 +D001P540Pieter V 153601Winter Maja Sophie 00056000000 000000AUT* +B110Dressurreiterprüfung L CDN-C_Ne003000000011 +C110035110038705000000000000000000000000000000000000000000000000 +D0011317Quality's Finest 612295Stroblmair Victoria 00074000000 000000AUT* +D0021F34Ferro Felicis 146066Gstöttenbauer Olivia 00064000000 000000AUT* +D003P540Pieter V 153601Winter Maja Sophie 00062000000 000000AUT* +B121Dressurprüfung L CDN-C_Ne003000000012 +C121035110038705000000000000000000000000000000000000000000000000 +D001AN19Exklusiv EM 187665Mück Hannah 00068000000 000000AUT* +D0021F34Ferro Felicis 146066Gstöttenbauer Olivia 00062000000 000000AUT* +D003AE11Merlin SH 061601Povacz Gisela 00060000000 000000AUT* +B122Dressurprüfung L CDN-C_Ne002000000012 +C122035110038705000000000000000000000000000000000000000000000000 +D001A024D Day 075374Ambros Susanne 00066000000 000000AUT*10071068 108EH50 +D0023966Capitaine 601366Madlmayr Carina 00060000000 000000AUT* +B131Dressurpferdeprüfung A CDN-C_Ne003000000013 +C131035110038705000000000000000000000000000000000000000000000000 +D001AX99Bon Sai 102783Steyrer Anna 00073400000 000000AUT* +D0020214SHS Donna Verdi 169981Süss Sarah 00066000000 000000AUT* +D003PT24Daneder's Captain 146663Steinmetz Sinah-Marie 00061200000 000000AUT* +B132Dressurpferdeprüfung A CDN-C_Ne005000000013 +C132035110038705000000000000000000000000000000000000000000000000 +D001MAXIVerstappen 2 075374Ambros Susanne 00074600000 000000AUT*10071068 +D0022H08SHS Weltmädel 169981Süss Sarah 00074200000 000000AUT* +D003P983Daneders Tornado 153601Winter Maja Sophie 00064800000 000000AUT* +D0044B03SHS Roubinjo 169981Süss Sarah 00064200000 000000AUT* +D0054B66Vingino's Victory 616957Kiesenhofer Sarah 00063000000 000000AUT* +B140Dressurpferdeprüfung L CDN-C_Ne003000000014 +C140035110038705000000000000000000000000000000000000000000000000 +D0012H08SHS Weltmädel 169981Süss Sarah 00067800000 000000AUT* +D002P983Daneders Tornado 153601Winter Maja Sophie 00064800000 000000AUT* +D0034B03SHS Roubinjo 169981Süss Sarah 00063000000 000000AUT* diff --git a/docs/02_Domain/Events/Neumarkt2026/26129.md b/docs/02_Domain/Events/Neumarkt2026/26129.md new file mode 100644 index 00000000..d0180322 --- /dev/null +++ b/docs/02_Domain/Events/Neumarkt2026/26129.md @@ -0,0 +1,70 @@ +# CDN-C NEU / CDNP-C NEU NEUMARKT/M., OÖ + +**Turnier-Nr.: 26129** | **Datum: 26. April 2026** + +## Allgemeine Informationen + +* **Veranstalter**: Union Reit- u. Fahrverein Neumarkt/M. (6-009) +* **Ort**: Reitanlage Stroblmair, 4212 Neumarkt +* **Kontaktadresse**: Ursula Stroblmair, Brandstetterweg 2, 4212 Neumarkt + * **Telefon**: 0664 1832381 + * **E-Mail**: reit-stall@gmx.at +* **Nennungsschluss**: 25.04.2026, 19:00 Uhr +* **Online-Nennung**: Ab Mittwoch, 22.04. auf www.ihremeldestelle.at möglich +* **Meldestelle**: Geöffnet ab 25.04., 17:00 Uhr (Tel: +43 681 10769120) +* **Start- und Ergebnislisten**: Ab 20:30 Uhr auf www.ihremeldestelle.at verfĂŒgbar + +## Technische Details und GebĂŒhren + +* **Austragungsplatz**: 20 x 60 m Sand/Vlies +* **Vorbereitungsplatz**: 20 x 40 m Halle (Sand/Vlies) und 20 x 60 m (Sand/Vlies) +* **Boxen**: Keine Einstallung möglich +* **Kosten**: Startgeld € 15,- pro Bewerb; kein Nenngeld und kein Sporteuro + +## FunktionĂ€re + +* **Turnierleiter**: Ursula Stroblmair +* **Turnierbeauftragte**: Alexandra Schuster +* **Richter**: Alexandra Schuster, Ulrike KnasmĂŒller-Prinz, Karin Wallner +* **Steward**: Barbara Hruschka +* **Tierarzt**: Dr. Sabine Ötschmaier + +--- + +## Besondere Bestimmungen + +* **Teilnahmevoraussetzungen**: + * FĂŒr Reiterpass-/Reiternadel-Aufgaben ist die Mitgliedschaft bei einem OEPS-Verein und der Besitz des + Reiterpasses erforderlich + * Pferde fĂŒr Reiterpass-/Reiternadel-Aufgaben mĂŒssen nicht beim OEPS registriert sein +* **Pferde**: + * Ein Pferd darf pro Tag maximal 3x starten + * Ein Pferd darf mit zwei verschiedenen Reitern an den Start gehen + * Vorlage des Pferdepasses mit gĂŒltigem Impfschutz gemĂ€ĂŸ § 11 OTO ist Pflicht + * Jedes teilnehmende Pferd muss haftpflichtversichert sein +* **Haftung**: Der Veranstalter ĂŒbernimmt keine Haftung jeder Art und Ursache. + Teilnehmer und Besitzer haften persönlich fĂŒr SchĂ€den gegenĂŒber Dritten +* **Sonstiges**: Es gilt Leinenpflicht fĂŒr Hunde auf dem gesamten GelĂ€nde. + AuslĂ€ndische Equiden unterliegen der TRACES-Pflicht. + +--- + +## Bewerbe (Sonntag, 26. April 2026 - Beginn 08:00 Uhr) + +| Nr. | Bewerb | Aufg. | Details / Abteilungen | +|:-------|:---------------------------------|:------|:-------------------------------------------------| +| **1** | DressurreiterprĂŒfung Reiterpass | R1 | RV: A § 103/5 | +| **2** | DressurreiterprĂŒfung Reiternadel | R4 | RV: A § 103/5 | +| **3** | DressurreiterprĂŒfung lizenzfrei | LF1 | RV: A § 103/5 | +| **4** | DressurreiterprĂŒfung lizenzfrei | LF3 | RV: A § 103/5 | +| **5** | First Ridden | - | | +| **6** | FĂŒhrzĂŒgelklasse | - | | +| **7** | Pony DressurprĂŒfung Kl. A | P1 | RV: A, § 901 | +| **8** | DressurreiterprĂŒfung Kl. A | DRA1 | 1. Abt: R1/RD1; 2. Abt: R2/RD2 u. höher | +| **9** | DressurprĂŒfung Kl. A | A5 | 1. Abt: R1/RD1; 2. Abt: R2/RD2 u. höher | +| **13** | DressurpferdeprĂŒfung Kl. A | DPA1 | 1. Abt: 4-jĂ€hr. Pferde; 2. Abt: 5-6-jĂ€hr. Pferde | +| **14** | DressurpferdprĂŒfung Kl. L | DPL1 | FĂŒr 5-6-jĂ€hr. Pferde | +| **10** | Pony DressurprĂŒfung Kl. L | P6 | RV: A, § 901 | +| **11** | DressurreiterprĂŒfung Kl. L | DRL1 | 1. Abt: R1/RD1; 2. Abt: R2/RD2 u. höher | +| **12** | DressurprĂŒfung Kl. L | L3 | 1. Abt: R1/RD1; 2. Abt: R2/RD2 u. höher | + diff --git a/docs/02_Domain/Events/Neumarkt2026/26129.pdf b/docs/02_Domain/Events/Neumarkt2026/26129.pdf new file mode 100644 index 00000000..74be9d08 Binary files /dev/null and b/docs/02_Domain/Events/Neumarkt2026/26129.pdf differ diff --git a/docs/02_Domain/Events/Neumarkt2026/Neumarkt-Logo.png b/docs/02_Domain/Events/Neumarkt2026/Neumarkt-Logo.png new file mode 100644 index 00000000..e19ed1c9 Binary files /dev/null and b/docs/02_Domain/Events/Neumarkt2026/Neumarkt-Logo.png differ diff --git a/docs/02_Domain/Events/Neumarkt2026/_26128.pdf b/docs/02_Domain/Events/Neumarkt2026/_26128.pdf new file mode 100644 index 00000000..624dbe06 Binary files /dev/null and b/docs/02_Domain/Events/Neumarkt2026/_26128.pdf differ diff --git a/docs/02_Domain/Events/Neumarkt2026/_26129.pdf b/docs/02_Domain/Events/Neumarkt2026/_26129.pdf new file mode 100644 index 00000000..2c348ee2 Binary files /dev/null and b/docs/02_Domain/Events/Neumarkt2026/_26129.pdf differ diff --git a/docs/02_Domain/Events/St-Poetlen-Hart-2026/26354.pdf b/docs/02_Domain/Events/St-Poetlen-Hart-2026/26354.pdf new file mode 100644 index 00000000..f0a11a80 Binary files /dev/null and b/docs/02_Domain/Events/St-Poetlen-Hart-2026/26354.pdf differ diff --git a/docs/02_Domain/Events/St-Poetlen-Hart-2026/26355.pdf b/docs/02_Domain/Events/St-Poetlen-Hart-2026/26355.pdf new file mode 100644 index 00000000..428685f4 Binary files /dev/null and b/docs/02_Domain/Events/St-Poetlen-Hart-2026/26355.pdf differ diff --git a/docs/02_Domain/Events/St-Poetlen-Hart-2026/logo-Melanie-Riedl.jpeg b/docs/02_Domain/Events/St-Poetlen-Hart-2026/logo-Melanie-Riedl.jpeg new file mode 100644 index 00000000..a148188e Binary files /dev/null and b/docs/02_Domain/Events/St-Poetlen-Hart-2026/logo-Melanie-Riedl.jpeg differ diff --git a/docs/02_Domain/Events/St-Poetlen-Hart-2026/logo_reitclubstpoeltenhart.png b/docs/02_Domain/Events/St-Poetlen-Hart-2026/logo_reitclubstpoeltenhart.png new file mode 100644 index 00000000..311bca17 Binary files /dev/null and b/docs/02_Domain/Events/St-Poetlen-Hart-2026/logo_reitclubstpoeltenhart.png differ diff --git a/docs/03_Development/Backend/API/API_Uebersicht_Stammdaten.md b/docs/03_Development/Backend/API/API_Uebersicht_Stammdaten.md new file mode 100644 index 00000000..48a478a1 --- /dev/null +++ b/docs/03_Development/Backend/API/API_Uebersicht_Stammdaten.md @@ -0,0 +1,63 @@ +--- +type: Reference +status: ACTIVE +owner: Backend Developer +last_update: 2026-04-03 +--- + +# API-Übersicht Stammdaten + +Abdeckung: Backend Sprint B‑1 (abgeschlossen). Konsolidierte Endpunkte fĂŒr Reiter, Pferde, Vereine und FunktionĂ€re. Implementiert mit Ktor; DTOs und Validierung in den jeweiligen Services. + +Hinweis Tenant/Headers: Stammdaten sind global bzw. nicht tenant‑spezifisch. FĂŒr domain‑spezifische VorgĂ€nge (Entries, Kassa) gilt ADR‑0021/`X-Event-Id`. + +## Reiter (`/reiter`) + +- GET `/reiter` — Liste (optional Filter: `lizenzKlasse`, `vereinId`; Pagination: `limit`, `offset`) +- GET `/reiter/search?q=...` — Suche nach Name oder Satznummer +- GET `/reiter/{id}` — Einzelabruf +- GET `/reiter/satznummer/{nr}` — Lookup per Satznummer +- POST `/reiter` — Anlegen +- PUT `/reiter/{id}` — Aktualisieren (partiell) +- DELETE `/reiter/{id}` — Löschen + +Quelle: `backend/services/masterdata/masterdata-api/.../ReiterController.kt` + +## Pferde (`/horse`) + +- GET `/horse` — Liste (optional Filter: `jahrgang`, `besitzerId`; Pagination: `limit`, `offset`) +- GET `/horse/search?q=...` — Suche nach Name/Nummern +- GET `/horse/{id}` — Einzelabruf +- POST `/horse` — Anlegen +- PUT `/horse/{id}` — Aktualisieren (partiell) +- DELETE `/horse/{id}` — Löschen + +Quelle: `backend/services/masterdata/masterdata-api/.../HorseController.kt` + +## Vereine (`/verein`) + +- GET `/verein` — Liste (optional Filter: `verband`/Bundesland; Pagination: `limit`, `offset`) +- GET `/verein/search?q=...` — Suche nach Name/Kurzname +- GET `/verein/{id}` — Einzelabruf +- POST `/verein` — Anlegen +- PUT `/verein/{id}` — Aktualisieren (partiell) +- DELETE `/verein/{id}` — Löschen + +Quelle: `backend/services/masterdata/masterdata-api/.../VereinController.kt` + +## FunktionĂ€re (`/funktionaer`) + +- GET `/funktionaer` — Liste (optional Filter: `rolle`; Pagination: `limit`, `offset`) +- GET `/funktionaer/search?q=...` — Suche nach Name +- GET `/funktionaer/{id}` — Einzelabruf +- POST `/funktionaer` — Anlegen +- PUT `/funktionaer/{id}` — Aktualisieren (partiell) +- DELETE `/funktionaer/{id}` — Löschen + +Quelle: `backend/services/masterdata/masterdata-api/.../FunktionaerController.kt` + +## Standards + +- Auth: Standard-Bearer optional je nach Deploymentprofil (siehe Service-Konfig) +- Content-Type: `application/json; charset=utf-8` +- FehlerfĂ€lle: 400 (Validierung), 404 (nicht gefunden) diff --git a/docs/03_Development/Backend/API/Kassa_API.md b/docs/03_Development/Backend/API/Kassa_API.md new file mode 100644 index 00000000..c577fb6e --- /dev/null +++ b/docs/03_Development/Backend/API/Kassa_API.md @@ -0,0 +1,37 @@ +--- +type: Reference +status: DRAFT +owner: Backend Developer +last_update: 2026-04-03 +--- + +# Kassa-API (Entwurf) + +AbhĂ€ngigkeit: Backend Sprint B‑2 (Kassa-Service) — laut Curator‑Roadmap noch offen. Dieses Dokument reserviert die Endpunkte und beschreibt die Erwartungen. Die Implementierungsdetails werden ergĂ€nzt, sobald der Service fertiggestellt ist. + +Mandantenkontext: Erfordert `X-Event-Id` (ADR‑0021). Alle Kassa‑Operationen sind tenant‑lokal. + +## Endpunkte (geplant) + +- GET `/kassa/saldo` — Liefert den aktuellen Saldo der Veranstaltung und optional pro Turnier. + - Query: `turnierId` (optional, UUID) + - Response: `{ saldoCents: long, currency: "EUR", scope: "veranstaltung"|"turnier", turnierId?: UUID }` + +- GET `/zahlvorgaenge` — Listet Kassa‑Buchungen/ZahlvorgĂ€nge (paginiert, filterbar). + - Query: `turnierId?`, `teilnehmerId?`, `typ?` (EINZAHLUNG|AUSZAHLUNG|KORREKTUR), `limit`, `offset` + - Response: Liste von Transaktionen mit Betrag, Zeit, Verweis (Teilnehmer/Turnier), Notiz + +- POST `/zahlvorgaenge` — Erstellt einen neuen Zahlungsvorgang (Ein-/Auszahlung/Korrektur). + - Body: `{ typ, betragCents, currency, referenz: { teilnehmerId?|turnierId? }, notiz? }` + - Wirkung: Aktualisiert `teilnehmer_konten` bzw. `turnier_kassa` atomar. + +## Validierung & Regeln (voraussichtlich) + +- Atomare Konsistenz: Transaktion + Saldo‑Update innerhalb einer DB‑Transaktion. +- Negativsaldo optional durch Feature‑Flag blockierbar. +- Audit‑Trail pro Vorgang (who/when), Idempotenz‑Key optional. + +## Implementierungsstand + +- Datenstrukturen `teilnehmer_konten` und `turnier_kassa` sind per Flyway angelegt (siehe [Datenbankschema](../Schema/Database_Schema_V1-V009.md)). +- Endpunkte folgen nach Abschluss Backend B‑2. Dieses Dokument wird dann auf `status: ACTIVE` gesetzt. diff --git a/docs/03_Development/Backend/Guides/Database_Best_Practices.md b/docs/03_Development/Backend/Guides/Database_Best_Practices.md new file mode 100644 index 00000000..c041a9f2 --- /dev/null +++ b/docs/03_Development/Backend/Guides/Database_Best_Practices.md @@ -0,0 +1,59 @@ +--- +type: Guide +status: DRAFT +owner: Backend Developer +date: 2026-02-02 +last_update: 2026-03-15 +--- + +# Database Best Practices & Exposed 1.0.0 + +Dieser Guide beschreibt den korrekten Umgang mit der Datenbank-Schicht in unseren Backend-Services, basierend auf JetBrains Exposed 1.0.0. + +## 1. Architektur-Prinzipien + +* **Trennung:** Datenbank-Zugriffe gehören ausschließlich in die `infrastructure/persistence` Schicht. Services nutzen Repositories (Interfaces), keine direkten Exposed-Aufrufe. +* **Transaktionen:** Jede geschĂ€ftliche Operation sollte in einer Transaktion laufen. Nutze dafĂŒr die Helper aus `DatabaseUtils.kt`. + +## 2. Nutzung von `DatabaseUtils` + +Wir haben zentrale Wrapper fĂŒr Transaktionen, um Fehlerbehandlung und Logging zu vereinheitlichen. + +### 2.1 Transaktionen starten + +Nutze immer `transactionResult` (oder die Aliase `readTransaction` / `writeTransaction`), um Exposed-Code auszufĂŒhren. + +```kotlin +fun findUser(id: UUID): Result = readTransaction { + // 'this' ist hier eine JdbcTransaction + UserTable.select { UserTable.id eq id } + .map { /* row -> User(...) */ } + .singleOrNull() +} +``` + +**Wichtig:** Der Lambda-Receiver ist `JdbcTransaction`. Das ermöglicht Zugriff auf Low-Level JDBC Funktionen, falls nötig. + +### 2.2 Low-Level SQL (`exec`, `executeUpdate`) + +Vermeide rohes SQL, wo immer möglich. Wenn es sein muss (z.B. fĂŒr Performance-Optimierungen oder spezielle Postgres-Features), beachte folgende Regeln fĂŒr Exposed 1.0.0: + +* **`exec`:** Nutze immer `explicitStatementType`. + ```kotlin + this.exec("SELECT 1", explicitStatementType = StatementType.SELECT) { rs -> /* handle ResultSet */ } + ``` +* **`executeUpdate`:** Nutze die Helper-Methode `DatabaseUtils.executeUpdate`, da sie sich um das korrekte Schließen von Statements kĂŒmmert (Exposed `PreparedStatementApi` ist nicht `AutoCloseable`). + +## 3. Exposed 1.0.0 Besonderheiten + +* **UUIDs:** Nutze `Table.javaUUID()` fĂŒr `java.util.UUID` Spalten. `Table.uuid()` ist fĂŒr `kotlin.uuid.Uuid` reserviert. +* **JSONB:** Bei SQLite wird JSON automatisch gewrappt. PrĂŒfe `castToJsonFormat` Flag. + +## 4. Fehlerbehandlung + +`DatabaseUtils` fĂ€ngt `SQLException` ab und mappt sie auf unsere Domain-Fehler (`ErrorDto`): +* Duplicate Key -> `ErrorCodes.DUPLICATE_ENTRY` +* Foreign Key -> `ErrorCodes.FOREIGN_KEY_VIOLATION` +* Timeout -> `ErrorCodes.DATABASE_TIMEOUT` + +Wirf keine rohen Exceptions aus Repositories. diff --git a/docs/03_Development/Backend/Guides/Testing_with_Postman.md b/docs/03_Development/Backend/Guides/Testing_with_Postman.md new file mode 100644 index 00000000..949de52f --- /dev/null +++ b/docs/03_Development/Backend/Guides/Testing_with_Postman.md @@ -0,0 +1,18 @@ +--- +type: Guide +status: REDIRECT +owner: Curator +tags: [testing, postman, backend, api] +last_update: 2026-04-03 +--- + +# đŸ§Ș Testanleitung: Ping‑Service & Gateway mit Postman (Weiterleitung) + +Diese Seite wurde in ein zentrales Runbook fĂŒr Betriebsanleitungen ĂŒberfĂŒhrt. + +Bitte nutze ab sofort das konsolidierte Runbook: + +- Runbook: API‑Tests mit Postman + `docs/07_Infrastructure/runbooks/POSTMAN_API_Tests_Runbook.md` + +Hinweis: Dieser Eintrag bleibt als Verweis bestehen, damit bestehende Links nicht brechen. diff --git a/docs/03_Development/Backend/Multi_Tenant_Kurz.md b/docs/03_Development/Backend/Multi_Tenant_Kurz.md new file mode 100644 index 00000000..0c4a6455 --- /dev/null +++ b/docs/03_Development/Backend/Multi_Tenant_Kurz.md @@ -0,0 +1,28 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +last_update: 2026-04-03 +--- + +# Tenant-Isolation & Multi‑Tenant (Kurzfassung) + +VollstĂ€ndige Entscheidung: [ADR‑0021: Tenant‑Resolution‑Strategie (Schema‑per‑Tenant)](../01_Architecture/adr/0021-tenant-resolution-strategy-de.md). + +## Kernaussagen + +- Eine Veranstaltung = ein Tenant = ein Datenbankschema (Schema‑per‑Tenant). +- Requests tragen `X-Event-Id`; Backend validiert gegen `control.tenants` und schaltet das Schema je Request. +- Flyway fĂŒhrt Migrationen je Tenant‑Schema aus (eigene `flyway_schema_history`). +- Stammdaten (Reiter/Pferde/Vereine/FunktionĂ€re) sind global und nicht tenant‑spezifisch; Entries/Kassa sind tenant‑lokal. + +## Umsetzung (Kurz) + +- Web‑Layer: `TenantWebFilter` (Spring) bzw. Plugin (Ktor) liest `X-Event-Id` und legt `TenantContext` ab. +- Persistence: SCHEMA‑Multitenancy (`SET search_path`) oder Hibernate‑`MultiTenantConnectionProvider`. +- Registry: `control.tenants(event_id, schema_name, status, db_url?, version, created_at)`. + +## Betroffene Bereiche + +- Datenmodell tenant‑lokal: `veranstaltungen`, `turniere`, `bewerbe`, `abteilungen`, `teilnehmer_konten`, `turnier_kassa` (siehe [Datenbankschema](./Schema/Database_Schema_V1-V009.md)). +- Services: Der Entries‑Service arbeitet mandantenfĂ€hig; andere Services bleiben Single‑Tenant/global (vgl. Backend‑Roadmap). diff --git a/docs/03_Development/Backend/README.md b/docs/03_Development/Backend/README.md new file mode 100644 index 00000000..2b2e7cff --- /dev/null +++ b/docs/03_Development/Backend/README.md @@ -0,0 +1,25 @@ +--- +type: Reference +status: ACTIVE +owner: Backend Developer +last_update: 2026-03-15 +--- +# Backend Dokumentation + +Dieses Verzeichnis enthĂ€lt die spezifische Dokumentation fĂŒr alle Backend-Komponenten, einschließlich der Microservices und der Infrastruktur-Module wie dem API-Gateway. + +## Struktur + +* `Services/`: EnthĂ€lt pro Service eine dedizierte Markdown-Datei, die dessen Zweck, API, Datenmodell und Konfiguration beschreibt. +* `API/`: Querliegende API-Referenzen und Übersichten (Stammdaten, Kassa, usw.). +* `Schema/`: Datenbankschemata und MigrationsĂŒbersichten (Flyway). +* `Integration/`: Dokumentation zur Interaktion zwischen den Services (z.B. Event-Flows). + +## Wichtige Einstiegspunkte + +* **[Ping-Service](./Services/PingService_Reference.md):** Dient als technischer Blueprint und einfachstes Beispiel fĂŒr einen Service. +* **[API-Gateway](../07_Infrastructure/api-gateway.md):** Beschreibung des zentralen Einstiegspunkts fĂŒr alle externen Anfragen. +* **[Stammdaten-APIs (Reiter, Pferde, Vereine, FunktionĂ€re)](./API/API_Uebersicht_Stammdaten.md):** Konsolidierte Endpunkt-Übersicht (Backend B‑1 abgeschlossen). +* **[Datenbankschema V1–V009](./Schema/Database_Schema_V1-V009.md):** Tabellen und Constraints (veranstaltungen, turniere, bewerbe, abteilungen, teilnehmer_konten, turnier_kassa). +* **[Kassa-API](./API/Kassa_API.md):** Platzhalter fĂŒr Saldo/Transaktionen; wird ergĂ€nzt, sobald Backend B‑2 Kassa fertig ist. +* **[Tenant-Isolation & Multi‑Tenant kurz](./Multi_Tenant_Kurz.md):** Zusammenfassung gem. ADR‑0021. diff --git a/docs/03_Development/Backend/Schema/Database_Schema_V1-V009.md b/docs/03_Development/Backend/Schema/Database_Schema_V1-V009.md new file mode 100644 index 00000000..f9b5cb5c --- /dev/null +++ b/docs/03_Development/Backend/Schema/Database_Schema_V1-V009.md @@ -0,0 +1,130 @@ +--- +type: Reference +status: ACTIVE +owner: Backend Developer +last_update: 2026-04-03 +--- + +# Datenbankschema V1–V009 (Tenant‑Schema) + +Quelle: Flyway‑Migrationen im Entries‑Service (`backend/services/entries/entries-service/src/main/resources/db/tenant/`), insbesondere `V2__domain_hierarchy.sql`. + +Hinweis zur Architektur: Je Veranstaltung (Tenant) existiert ein eigenes Datenbankschema (ADR‑0021). Alle untenstehenden Tabellen werden pro Tenant‑Schema angelegt und sind somit mandantengetrennt. + +## TabellenĂŒbersicht + +- `veranstaltungen` — Eine Veranstaltung (Singleton im Tenant‑Schema) +- `turniere` — Turniere einer Veranstaltung (1:N zu `veranstaltungen`) +- `bewerbe` — Bewerbe/PrĂŒfungen eines Turniers (1:N zu `turniere`) +- `abteilungen` — Abteilungen/Heats eines Bewerbs (1:N zu `bewerbe`) +- `teilnehmer_konten` — Aggregierte Salden eines Teilnehmers ĂŒber alle Turniere der Veranstaltung +- `turnier_kassa` — Kassa‑Saldo pro Turnier + +## Detaillierte Definitionen (aus V2__domain_hierarchy.sql) + +### veranstaltungen +PrimĂ€rschlĂŒssel: `id (UUID)` + +Spalten: +- `id UUID PRIMARY KEY` +- `created_at TIMESTAMPTZ NOT NULL` +- `updated_at TIMESTAMPTZ NOT NULL` + +### turniere +PrimĂ€rschlĂŒssel: `id (UUID)` +FremdschlĂŒssel: `veranstaltung_id → veranstaltungen(id) ON DELETE CASCADE` + +Spalten: +- `id UUID PRIMARY KEY` +- `veranstaltung_id UUID NOT NULL` +- `oeps_turniernummer VARCHAR(50) NOT NULL` +- `created_at TIMESTAMPTZ NOT NULL` +- `updated_at TIMESTAMPTZ NOT NULL` + +Indizes/Constraints: +- `UNIQUE (oeps_turniernummer)` → `uq_turniere_oeps_nr` +- `INDEX (veranstaltung_id)` → `idx_turniere_veranstaltung_id` + +### bewerbe +PrimĂ€rschlĂŒssel: `id (UUID)` +FremdschlĂŒssel: `turnier_id → turniere(id) ON DELETE CASCADE` + +Spalten: +- `id UUID PRIMARY KEY` +- `turnier_id UUID NOT NULL` +- `klasse VARCHAR(50) NOT NULL` +- `hoehe_cm INTEGER NULL` +- `bezeichnung TEXT NOT NULL` +- `created_at TIMESTAMPTZ NOT NULL` +- `updated_at TIMESTAMPTZ NOT NULL` + +Indizes: +- `INDEX (turnier_id)` → `idx_bewerbe_turnier_id` +- `INDEX (klasse)` → `idx_bewerbe_klasse` + +### abteilungen +PrimĂ€rschlĂŒssel: `id (UUID)` +FremdschlĂŒssel: `bewerb_id → bewerbe(id) ON DELETE CASCADE` + +Spalten: +- `id UUID PRIMARY KEY` +- `bewerb_id UUID NOT NULL` +- `nr INTEGER NOT NULL` +- `bezeichnung TEXT NOT NULL` +- `typ VARCHAR(32) NOT NULL` (Werte: `SEPARATE_SIEGEREHRUNG`, `ORGANISATORISCH`) +- `created_at TIMESTAMPTZ NOT NULL` +- `updated_at TIMESTAMPTZ NOT NULL` + +Constraints/Indizes: +- `CHECK (typ IN ('SEPARATE_SIEGEREHRUNG','ORGANISATORISCH'))` → `chk_abteilungen_typ` +- `UNIQUE (bewerb_id, nr)` → `uq_abteilungen_bewerb_nr` +- `INDEX (bewerb_id)` → `idx_abteilungen_bewerb_id` +- `INDEX (typ)` → `idx_abteilungen_typ` + +### teilnehmer_konten +PrimĂ€rschlĂŒssel: `id (UUID)` +FremdschlĂŒssel: `veranstaltung_id → veranstaltungen(id) ON DELETE CASCADE` + +Spalten: +- `id UUID PRIMARY KEY` +- `veranstaltung_id UUID NOT NULL` +- `teilnehmer_id UUID NOT NULL` +- `saldo_cents BIGINT NOT NULL DEFAULT 0` +- `currency CHAR(3) NOT NULL DEFAULT 'EUR'` +- `created_at TIMESTAMPTZ NOT NULL` +- `updated_at TIMESTAMPTZ NOT NULL` + +Indizes/Constraints: +- `UNIQUE (veranstaltung_id, teilnehmer_id)` → `uq_tkonten_veranstaltung_teilnehmer` +- `INDEX (veranstaltung_id)` → `idx_tkonten_veranstaltung_id` +- `INDEX (teilnehmer_id)` → `idx_tkonten_teilnehmer_id` + +### turnier_kassa +PrimĂ€rschlĂŒssel: `id (UUID)` +FremdschlĂŒssel: `turnier_id → turniere(id) ON DELETE CASCADE` + +Spalten: +- `id UUID PRIMARY KEY` +- `turnier_id UUID NOT NULL` +- `saldo_cents BIGINT NOT NULL DEFAULT 0` +- `currency CHAR(3) NOT NULL DEFAULT 'EUR'` +- `created_at TIMESTAMPTZ NOT NULL` +- `updated_at TIMESTAMPTZ NOT NULL` + +Indizes/Constraints: +- `UNIQUE (turnier_id)` → `uq_turnier_kassa_turnier` +- `INDEX (turnier_id)` → `idx_turnier_kassa_turnier_id` + +## Versionierung / Flyway + +- Die oben dokumentierten Tabellen sind in `V2__domain_hierarchy.sql` definiert. +- Weitere Migrationen V1–V009 betreffen Bootstrap/Erweiterungen; diese Seite wird fortlaufend ergĂ€nzt, sobald neue fachrelevante Strukturen hinzukommen. + +## Beziehungen (Kurz) + +`veranstaltungen (1) ──< (N) turniere (1) ──< (N) bewerbe (1) ──< (N) abteilungen` + +Separat aggregierend: + +- `teilnehmer_konten` auf Veranstaltungsebene (pro Teilnehmer genau ein Konto) +- `turnier_kassa` auf Turnierebene (pro Turnier genau ein Kassa‑Eintrag) diff --git a/docs/03_Development/Backend/Services/PingService_Reference.md b/docs/03_Development/Backend/Services/PingService_Reference.md new file mode 100644 index 00000000..babb2e2d --- /dev/null +++ b/docs/03_Development/Backend/Services/PingService_Reference.md @@ -0,0 +1,121 @@ +--- +type: Reference +status: ACTIVE +owner: Backend Developer +tags: [backend, service, reference, ping] +last_update: 2026-04-03 +--- + +# 🎯 Ping Service Reference + +Der `ping-service` ist der **"Tracer Bullet"** (Leuchtspurgeschoss) der Meldestelle-Architektur. Er ist kein Wegwerf-Prototyp, sondern die **Referenzimplementierung** fĂŒr alle zukĂŒnftigen Microservices. + +## 1. Mission & Verantwortung + +* **Technischer Durchstich:** Beweist, dass die Kette *Frontend -> Gateway -> Service -> DB* funktioniert. +* **Blueprint:** Definiert Standards fĂŒr Architektur (DDD/Hexagonal), Testing, Security und Build-Prozesse. +* **Infrastruktur-Validierung:** Testet die Integration mit Consul, Keycloak, Postgres, Redis und Zipkin. +* **Offline-First Lab:** Hier wird die Delta-Sync-Logik (`/sync`) entwickelt und validiert, bevor sie in fachliche Services einzieht. + +--- + +## 2. Technologie-Stack + +* **Framework:** Spring Boot 3.5.x (Spring MVC, Tomcat). +* **Sprache:** Kotlin 2.x (Coroutines fĂŒr asynchrone AblĂ€ufe). +* **Datenbank:** PostgreSQL (via Spring Data JPA). +* **Migration:** Flyway (mit service-spezifischer Historientabelle `flyway_schema_history_ping`). +* **Security:** OAuth2 Resource Server (JWT via Keycloak). +* **Resilience:** Resilience4j (Circuit Breaker). +* **API Contract:** KMP-Modul `:contracts:ping-api` (Shared Code mit Frontend). + +--- + +## 3. Architektur (Hexagonal) + +Der Service folgt strikt der **Ports & Adapters** (Hexagonal) Architektur: + +1. **Domain (`at.mocode.ping.domain`):** + * Der Kern. EnthĂ€lt Entities (`Ping`) und Business-Regeln. + * Frei von Frameworks (kein Spring, kein JPA). + * Definiert Interfaces fĂŒr Ports (`PingRepository`). + +2. **Application (`at.mocode.ping.application`):** + * Orchestriert die Use Cases (`PingUseCase`). + * Steuert Transaktionen (`@Transactional`). + * Verbindet Domain und Infrastructure. + +3. **Infrastructure (`at.mocode.ping.infrastructure`):** + * **Web:** `PingController` (REST API). + * **Persistence:** `PingRepositoryAdapter` (JPA Implementierung). + * **Security:** Global Config fĂŒr JWT-Validierung. + +--- + +## 4. API Endpunkte + +| Methode | Pfad | Auth | Beschreibung | +| :--- | :--- | :--- | :--- | +| `GET` | `/ping/simple` | 🔓 Public | Erstellt einen Ping in der DB. Testet Schreibzugriff. | +| `GET` | `/ping/enhanced` | 🔓 Public | Testet Circuit Breaker. Parameter `simulate=true` löst Fehler aus. | +| `GET` | `/ping/health` | 🔓 Public | Gibt Status "UP" zurĂŒck. | +| `GET` | `/ping/public` | 🔓 Public | Expliziter Public-Test. | +| `GET` | `/ping/secure` | 🔒 **Secure** | Erfordert Token mit Rolle `MELD_USER`. Testet Auth-Flow. | +| `GET` | `/ping/sync` | 🔒 **Secure** | **Delta-Sync**. Liefert Änderungen seit `lastSyncTimestamp`. | + +--- + +## 5. Getting Started + +### A. Voraussetzungen +* Java 25 (oder kompatibel). +* Docker & Docker Compose (fĂŒr Infrastruktur). + +### B. Infrastruktur starten +Bevor der Service laufen kann, braucht er Datenbank und Keycloak. +```bash +# Im Root-Verzeichnis +docker compose --profile infra up -d +``` + +### C. Starten via Gradle (Lokal) +Ideal fĂŒr Entwicklung und Debugging. +```bash +# Startet den Service im Profil "local" +./gradlew :backend:services:ping:ping-service:bootRun +``` +* **URL:** `http://localhost:8082` (Direktzugriff) +* **Debug Port:** 5006 + +### D. Starten via Docker (Integration) +Testet den Service im Container-Verbund (hinter dem Gateway). +```bash +# Baut das Image und startet es zusammen mit dem Gateway +docker compose --profile backend up -d --build +``` +* **URL (via Gateway):** `http://localhost:8081/api/ping/...` + +--- + +## 6. Konfiguration + +Die Konfiguration erfolgt primĂ€r ĂŒber `application.yml` und Environment-Variables (12-Factor App). + +| Variable | Default (Docker) | Beschreibung | +| :--- | :--- | :--- | +| `SERVER_PORT` | `8082` | Port des Services. | +| `POSTGRES_DB_URL` | `jdbc:postgresql://postgres:5432/...` | JDBC URL. | +| `SSEC_ISSUER_URI` | `http://keycloak:8080/...` | URL des Identity Providers (fĂŒr Token-Check). | +| `CONSUL_HOST` | `consul` | Host fĂŒr Service Discovery. | + +### Profile +* `local`: FĂŒr lokale Entwicklung (nutzt `localhost` Adressen). +* `docker`: FĂŒr Betrieb im Docker-Netzwerk (nutzt Service-Namen wie `postgres`). +* `test`: FĂŒr Unit/Integration-Tests (nutzt H2 oder Testcontainers). + +--- + +## 7. Testing + +* **Unit Tests:** `./gradlew :backend:services:ping:ping-service:test` +* **Manuelle Tests:** Siehe [Testing with Postman](../Guides/Testing_with_Postman.md). diff --git a/docs/03_Development/Backend/_archive/2026-03-15_TASK_2026_Q1_Infrastructure_Hardening.md b/docs/03_Development/Backend/_archive/2026-03-15_TASK_2026_Q1_Infrastructure_Hardening.md new file mode 100644 index 00000000..8e5a9e83 --- /dev/null +++ b/docs/03_Development/Backend/_archive/2026-03-15_TASK_2026_Q1_Infrastructure_Hardening.md @@ -0,0 +1,80 @@ +--- +type: Task +status: ARCHIVED +owner: Senior Backend Developer +created: 2026-01-15 +completed: 2026-01-16 +priority: HIGH +context: Operation Tracer Bullet (Phase 1) +--- + +# Arbeitsanweisung: Infrastructure Hardening & Security Implementation + +**Ziel:** Finalisierung der Backend-Infrastruktur-Module und HĂ€rtung des `ping-service` gemĂ€ĂŸ [ADR 001](../../01_Architecture/adr/001-backend-infrastructure-decisions.md). + +--- + +## 1. Kontext & Architektur-Entscheidungen + +Wir befinden uns in **Phase 1** ("Tracer Bullet"). Das Ziel ist ein stabiler, sicherer Durchstich vom Frontend bis zur Datenbank. +Die Architektur wurde wie folgt geschĂ€rft (siehe ADR 001): +* **Persistence:** Hybrid-Ansatz (JPA fĂŒr Writes/Entities, Exposed fĂŒr komplexe Reads). +* **Security:** Zentralisiertes Modul (`backend/infrastructure/security`). +* **Messaging:** Kafka ist fĂŒr Phase 1 **out of scope**. Fokus auf REST. +* **Migration:** Flyway Skripte liegen direkt im Service (`db/migration`). + +--- + +## 2. Deine Aufgaben (Checkliste) + +### A. Security Module (`backend/infrastructure/security`) +Dieses Modul wurde neu angelegt. FĂŒlle es mit Leben. + +* [x] **Security Configuration:** + * Erstelle eine `SecurityConfig`-Klasse, die `SecurityFilterChain` konfiguriert. + * Implementiere OAuth2 Resource Server Support (JWT Validierung). + * Definiere globale CORS-Regeln (Frontend darf zugreifen). +* [x] **Role Converter:** + * Implementiere einen `KeycloakRoleConverter`, der die Rollen aus dem JWT (Realm/Resource Access) in Spring Security `GrantedAuthority` mapping. +* **Wichtig:** Achte auf KompatibilitĂ€t. Das Gateway nutzt WebFlux (Reactive), die Services nutzen WebMVC (Servlet). Falls nötig, trenne die Konfigurationen oder nutze `ConditionalOnWebApplication`. + +### B. Persistence Layer (`backend/infrastructure/persistence`) +Das Modul ist bereits konfiguriert. + +* [x] **Verwendung im Service:** + * Stelle sicher, dass der `ping-service` dieses Modul nutzt. + * Implementiere `PingEntity` als JPA Entity. + * Nutze `JpaRepository` fĂŒr Standard-CRUD-Operationen. + +### C. Ping Service Hardening (`backend/services/ping/ping-service`) +Mache den Service "Production Ready." + +* [x] **Flyway:** + * Erstelle `src/main/resources/db/migration/V1__init_ping.sql`. + * Definiere das Schema fĂŒr die `ping` Tabelle. +* [x] **API Implementation:** + * Implementiere `/ping/public` (offen) und `/ping/secure` (benötigt Auth). + * Nutze `@PreAuthorize("hasRole('MELD_USER')")` o.Ă€. zum Testen der Rollen. +* [x] **Resilience:** + * Konfiguriere Resilience4j (CircuitBreaker) fĂŒr die DB-Verbindung (via `application.yml`). + +### D. Gateway Integration (`backend/infrastructure/gateway`) +* [x] **Routing:** + * PrĂŒfe die `application.yml` im Gateway. + * Stelle sicher, dass Routen zum `ping-service` korrekt konfiguriert sind (via Service Discovery "ping-service"). + +--- + +## 3. Definition of Done + +1. Das Projekt kompiliert fehlerfrei (`./gradlew build`). +2. `docker compose up` startet Gateway, Ping-Service, Keycloak und Postgres ohne Fehler. +3. Ein Request auf `http://localhost:8080/ping/public` (via Gateway) liefert 200 OK. +4. Ein Request auf `http://localhost:8080/ping/secure` ohne Token liefert 401 Unauthorized. +5. Die Datenbank-Tabelle `ping` wurde durch Flyway automatisch erstellt. + +--- + +**Referenzen:** +* [ADR 001: Backend Infrastructure Decisions](../../01_Architecture/adr/001-backend-infrastructure-decisions.md) +* [Master Roadmap Q1 2026](../../01_Architecture/_archive/2026-03-15_MASTER_ROADMAP_2026_Q1.md) diff --git a/docs/03_Development/Frontend/ARCHITECTURE_RULES.md b/docs/03_Development/Frontend/ARCHITECTURE_RULES.md new file mode 100644 index 00000000..044296d9 --- /dev/null +++ b/docs/03_Development/Frontend/ARCHITECTURE_RULES.md @@ -0,0 +1,163 @@ +# Frontend-Architektur-Richtlinien + +> **Status:** Verbindlich ab 26.03.2026 +> **ZustĂ€ndig:** đŸ—ïž Lead Architect +> **Zweck:** Verhindert Architektur-Drift und inkonsistente Schichtentrennung. + +--- + +## Die 3 Schichten + +``` +frontend/ +├── core/ ← Infrastruktur (plattformĂŒbergreifend, kein Business-Code) +├── features/ ← Fachliche Bausteine (je ein Bounded Context) +└── shells/ ← AusfĂŒhrbare Apps (nur Verdrahtung, kein Fach-UI) +``` + +--- + +## Schicht 1: `core/` + +### Aufgabe + +Gemeinsame Infrastruktur, die von **allen** Features und Shells genutzt wird. + +### Module + +| Modul | Inhalt | +|-----------------|-----------------------------------------------------------------| +| `auth` | Login, Token-Management, OIDC/PKCE, `LoginScreen` | +| `design-system` | Farben, Typografie, gemeinsame UI-Komponenten, `SharedUiModels` | +| `domain` | Gemeinsame Domain-Modelle (plattformĂŒbergreifend) | +| `navigation` | `AppScreen`-Sealed-Class (einzige Wahrheit ĂŒber alle Routen) | +| `network` | Ktor-Client, `NetworkConfig` | +| `local-db` | SQLDelight/Room-Setup, `DatabaseProvider` | +| `sync` | Offline-Sync-Infrastruktur | + +### Regeln + +- ✅ Darf importieren: externe Libraries, andere `core`-Module (keine Zyklen) +- ❌ Darf NICHT importieren: `features/*`, `shells/*` +- ❌ Kein Business-Code, keine fachlichen Screens + +--- + +## Schicht 2: `features/` + +### Aufgabe + +Jedes Feature kapselt **einen Bounded Context** vollstĂ€ndig: Daten, Logik und UI. + +### Pflicht-Struktur eines Feature-Moduls + +``` +features/-feature/ +└── src/ + └── jvmMain/kotlin/at/mocode//feature/ + ├── data/ ← Repository, API-Client + ├── domain/ ← Modelle, Use Cases + ├── presentation/ ← ViewModel + Screen-Composables ← PFLICHT + └── di/ ← Koin-Module +``` + +### Vorhandene Features + +| Feature | Bounded Context | +|-------------------------|-----------------------------------------------| +| `ping-feature` | Verbindungstest / Sync-Status | +| `nennung-feature` | Nennungs-Erfassung am Turnier | +| `zns-import-feature` | ZNS-Stammdaten-Import | +| `veranstalter-feature` | Veranstalter-Auswahl, -Detail, -Neuanlage | +| `veranstaltung-feature` | Veranstaltungs-Übersicht, -Detail, -Neuanlage | +| `turnier-feature` | Turnier-Detail, alle Tabs, Akteure | + +### Regeln + +- ✅ Darf importieren: `core/*` +- ❌ Darf NICHT importieren: andere `features/*`, `shells/*` +- ✅ **Jedes Feature MUSS seinen eigenen Screen in `presentation/` haben** +- ❌ Screen-Composables gehören NICHT in den Shell + +--- + +## Schicht 3: `shells/` + +### Aufgabe + +Einstiegspunkt einer konkreten App. Verdrahtet Features und Core zu einer lauffĂ€higen Anwendung. + +### Erlaubter Inhalt im Shell + +``` +shells// +└── src/jvmMain/kotlin/at/mocode/desktop/ + ├── main.kt ← App-Einstiegspunkt, Koin-Init + ├── DesktopApp.kt ← Root-Composable, Login-Gate + ├── di/DesktopModule.kt ← Shell-spezifische DI + ├── navigation/ ← Navigation-Port (optional) + └── screens/ + ├── layout/DesktopMainLayout.kt ← Navigation + Layout-GerĂŒst + └── preview/ScreenPreviews.kt ← @Preview-Funktionen (IDE-only) +``` + +### Regeln + +- ✅ Darf importieren: `core/*`, `features/*` +- ✅ Darf enthalten: `main.kt`, `DesktopApp.kt`, DI-Verdrahtung, Layout, Previews +- ❌ Darf NICHT enthalten: fachliche Screen-Composables (gehören in Features) +- ❌ Darf NICHT enthalten: ViewModels, Repositories, Business-Logik + +--- + +## AbhĂ€ngigkeits-Diagramm + +``` +shells/meldestelle-desktop + ├── core/auth + ├── core/design-system + ├── core/domain + ├── core/navigation + ├── core/network + ├── core/local-db + ├── core/sync + ├── features/ping-feature + ├── features/nennung-feature + ├── features/zns-import-feature + ├── features/veranstalter-feature + ├── features/veranstaltung-feature + └── features/turnier-feature + +features/* → core/* (nur) +core/* → (keine internen AbhĂ€ngigkeiten außer erlaubte core-zu-core) +``` + +--- + +## Checkliste: Neues Feature anlegen + +1. `frontend/features/-feature/` Verzeichnis anlegen +2. `build.gradle.kts` nach Vorlage `nennung-feature` erstellen +3. Eintrag in `settings.gradle.kts` unter `// --- FEATURES ---` hinzufĂŒgen +4. Eintrag in `shells/meldestelle-desktop/build.gradle.kts` unter `// Feature-Module` hinzufĂŒgen +5. Screen in `presentation/` implementieren +6. DI-Modul in `di/` implementieren +7. DI-Modul in `shells/.../main.kt` registrieren +8. Route in `core/navigation/AppScreen.kt` eintragen +9. Navigation-Case in `shells/.../screens/layout/DesktopMainLayout.kt` eintragen + +--- + +## Anti-Patterns (verboten) + +| Anti-Pattern | Warum verboten | +|------------------------------------|------------------------------------------------------| +| Screen-Composable direkt im Shell | Verletzt Schichttrennung, nicht wiederverwendbar | +| Feature importiert anderes Feature | Erzeugt Kopplung, verhindert unabhĂ€ngige Entwicklung | +| `core` importiert `features` | ZirkulĂ€re AbhĂ€ngigkeit | +| Shared-Modelle im Shell definieren | Gehören in `core/design-system` oder `core/domain` | +| ViewModel im Shell | Gehört ins Feature | + +--- + +*Letzte Aktualisierung: 26.03.2026 — nach Architektur-Refactor (Screens aus Shell in Features verschoben)* diff --git a/docs/03_Development/Frontend/Diagrams/Frontend_Module_Map.drawio b/docs/03_Development/Frontend/Diagrams/Frontend_Module_Map.drawio new file mode 100644 index 00000000..52770221 --- /dev/null +++ b/docs/03_Development/Frontend/Diagrams/Frontend_Module_Map.drawio @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/03_Development/Frontend/Diagrams/Navigation_Wizard_Flows.drawio b/docs/03_Development/Frontend/Diagrams/Navigation_Wizard_Flows.drawio new file mode 100644 index 00000000..b8c67512 --- /dev/null +++ b/docs/03_Development/Frontend/Diagrams/Navigation_Wizard_Flows.drawio @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/03_Development/Frontend/E_Nennen/nennmaske-v01.html b/docs/03_Development/Frontend/E_Nennen/nennmaske-v01.html new file mode 100644 index 00000000..75cd39f8 --- /dev/null +++ b/docs/03_Development/Frontend/E_Nennen/nennmaske-v01.html @@ -0,0 +1,323 @@ + + + + + + Nennmaske Entwurf + + + + +
+ +
+
+
+ Pferd: + + + +
+
+
Keine Ergebnisse
+
+ +
+ +
+
+ Reiter: + + + +
+
+
Keine Ergebnisse
+
+ +
+ +
+
+
VERKAUF
+
BUCHUNGEN
+
+
+ 11 Artikel + +
+
+ + + + + + + + + + + + + + + + +
KNr+/-MengeBuchungstextBetrag
+Belastung0.00
+Gutschrift0.00
+Boxenpauschale0.00
+Ansage0.00
+
+
+
+ +
+ + + +
+ +
+
+
+
REITER
+
PFERD
+
BEWERBE
+
+
+ 0 Nennungen +
+ Positionieren + Stornieren +
+
+
+ + + + + + + + + + + +
TagPl.BewerbBewerbsnameBemerkungPferd
+
Keine Nennungen vorhanden
+
+
+ +
+
+ BewerbsĂŒbersicht +
+
+ 12 Bewerbe + Filtern +
+
+ + + + + + + + + + + + + + + + + +
TagPl.Bew.BeginnNenn.Bewerbsname
So1108:000DressurreiterprĂŒfung Ratepass
So1208:200DressurreiterprĂŒfung Katecnadel
So1308:400DressurreiterprĂŒfung Idf.
So1409:000DressurprĂŒfung Idf.
+
+
+ Bitte wÀhlen Sie zuerst ein Pferd und einen Reiter aus +
+
+
+ +
+ + + diff --git a/docs/06_Frontend/FIGMA/Vison_01/ATTRIBUTIONS.md b/docs/03_Development/Frontend/FIGMA/Vision_02/ATTRIBUTIONS.md similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/ATTRIBUTIONS.md rename to docs/03_Development/Frontend/FIGMA/Vision_02/ATTRIBUTIONS.md diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/NAVIGATION.md b/docs/03_Development/Frontend/FIGMA/Vision_02/NAVIGATION.md new file mode 100644 index 00000000..43c7745f --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_02/README.md b/docs/03_Development/Frontend/FIGMA/Vision_02/README.md new file mode 100644 index 00000000..2ea935d7 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/README.md @@ -0,0 +1,1024 @@ +# 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**: Veranstaltungen → Turniere → Bewerbe +- **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 + + + 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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/Login.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/Login.tsx new file mode 100644 index 00000000..225a5f63 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/Vison_01/src/app/components/NennungenTabelle.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/NennungenTabelle.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/NennungenTabelle.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/NennungenTabelle.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/NennungsMaske.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/NennungsMaske.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/NennungsMaske.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/NennungsMaske.tsx diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/PferdReiterEingabe.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/PferdReiterEingabe.tsx new file mode 100644 index 00000000..42488218 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/TurnierAnsicht.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/TurnierAnsicht.tsx new file mode 100644 index 00000000..a3d1724f --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/TurnierAnsicht.tsx @@ -0,0 +1,130 @@ +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 {PreislisteTab} from './turnier/PreislisteTab'; +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 && } + + + ); +} diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/TurnierErstellen.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/TurnierErstellen.tsx new file mode 100644 index 00000000..011cacef --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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 {PreislisteTab} from './turnier/PreislisteTab'; + +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/Vison_01/src/app/components/VerkaufBuchungen.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/VerkaufBuchungen.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/VerkaufBuchungen.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/VerkaufBuchungen.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/figma/ImageWithFallback.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/figma/ImageWithFallback.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/figma/ImageWithFallback.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/figma/ImageWithFallback.tsx diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/BewerbeTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/BewerbeTab.tsx new file mode 100644 index 00000000..469d1918 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/FunktionaereTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/FunktionaereTab.tsx new file mode 100644 index 00000000..08626363 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/OrganisationTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/OrganisationTab.tsx new file mode 100644 index 00000000..7158b8e0 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/PreislisteTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/PreislisteTab.tsx new file mode 100644 index 00000000..c6fec89d --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/StammdatenTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/StammdatenTab.tsx new file mode 100644 index 00000000..79c52070 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/StammdatenTab.tsx @@ -0,0 +1,831 @@ +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 MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import Autocomplete from '@mui/material/Autocomplete'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +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 FolderOpenIcon from '@mui/icons-material/FolderOpen'; + +// 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' +]; + +// Mock-Daten fĂŒr Vereine (spĂ€ter vom Backend) +const mockVereine = [ + 'RFV Neumarkt/Hausruck', + 'Reitclub Wien', + 'Reitverein Salzburg', + 'Pferdesportverband OÖ', + 'RC Linz', +]; + +interface StammdatenTabProps { + turnierId?: string; +} + +export function StammdatenTab({turnierId}: StammdatenTabProps) { + const [turniernummer, setTurniernummer] = useState(''); + const [turniernummerError, setTurniernummerError] = useState(''); + const [turnierTitel, setTurnierTitel] = useState(''); + const [kommentar, setKommentar] = useState(''); + const [typ, setTyp] = useState('national'); + const [sprache, setSprache] = useState('deutsch'); + + // Sparten (kombinierbar) + const [sparteDressur, setSparteDressur] = useState(false); + const [sparteSpringen, setSparteSpringen] = useState(false); + + // Klassen (kombinierbar!) + const [klasseC, setKlasseC] = useState(false); + const [klasseB, setKlasseB] = useState(false); + const [klasseA, setKlasseA] = useState(false); + + // Kategorien (Mehrfachauswahl!) + const [selectedKategorien, setSelectedKategorien] = useState([]); + + const [datumVon, setDatumVon] = useState(null); + const [datumBis, setDatumBis] = useState(null); + const [verein, setVerein] = useState(null); + const [logo, setLogo] = useState(''); + + // Vorschau nach Speichern + const [showVorschau, setShowVorschau] = useState(false); + + // BestĂ€tigungsdialog fĂŒr Initialisierung + const [showInitDialog, setShowInitDialog] = useState(false); + + // Initialisierungs-Status: Turnier ist initialisiert, wenn eine Turnier-Nr. vorhanden ist + const istNeu = turnierId === 'neu'; + const [turniernummerBestaetigt, setTurniernummerBestaetigt] = useState(false); + const istInitialisiert = !istNeu || turniernummerBestaetigt; + + // Turniernummer validieren + const validateTurniernummer = (value: string) => { + if (value.length === 0) { + setTurniernummerError(''); + return; + } + if (!/^\d+$/.test(value)) { + setTurniernummerError('Nur Zahlen erlaubt'); + return; + } + if (value.length !== 5) { + setTurniernummerError('Muss 5-stellig sein'); + return; + } + setTurniernummerError(''); + }; + + // VerfĂŒgbare Kategorien basierend auf Sparte UND Klasse + const verfuegbareKategorien = (() => { + const kategorien: string[] = []; + + // Sparte bestimmt die Basis-Kategorien + const basisKategorien: string[] = []; + if (sparteDressur) basisKategorien.push(...kategorienDressur); + if (sparteSpringen) basisKategorien.push(...kategorienSpringen); + + // Filter nach Klassen (C, B, A) + 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 => { + // Extrahiere die Klasse aus der Kategorie (z.B. "CSN-C-Neu" -> "C-Neu") + 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) => { + if (!istInitialisiert) return; + setSelectedKategorien(prev => + prev.includes(kategorie) + ? prev.filter(k => k !== kategorie) + : [...prev, kategorie] + ); + }; + + const handleInitialisieren = () => { + if (turniernummer.trim().length !== 5 || turniernummerError) return; + console.log('Turnier initialisieren mit Nr.:', turniernummer); + // TODO: Backend-Call zur Datenbank-Initialisierung + setTurniernummerBestaetigt(true); + }; + + const handleZuruecksetzen = () => { + setTurniernummer(''); + setTurniernummerError(''); + setTurnierTitel(''); + setKommentar(''); + setTyp('national'); + setSprache('deutsch'); + setSparteDressur(false); + setSparteSpringen(false); + setKlasseC(false); + setKlasseB(false); + setKlasseA(false); + setSelectedKategorien([]); + setDatumVon(null); + setDatumBis(null); + setVerein(null); + setLogo(''); + setShowVorschau(false); + }; + + const handleSpeichern = () => { + console.log('Turnier speichern:', { + turniernummer, + turnierTitel, + kommentar, + typ, + sprache, + sparteDressur, + sparteSpringen, + klasseC, + klasseB, + klasseA, + selectedKategorien, + datumVon, + datumBis, + verein, + logo, + }); + // TODO: Backend Integration + setShowVorschau(true); + }; + + return ( + + + {/* Vorschau nach Speichern (oben zentral) */} + {showVorschau && ( + + + + ✅ Turnier gespeichert + + + + + + {/* Turniernummer Badge */} + + + {turniernummer} + + + {selectedKategorien.slice(0, 5).map((kat, idx) => ( + + {kat} + + ))} + {selectedKategorien.length > 5 && ( + + +{selectedKategorien.length - 5} + + )} + + + + {/* Turnier-Titel */} + + {turnierTitel || 'FrĂŒhjahrs-Turnier 2026'} + + + {/* Sparten & Klassen */} + + {sparteDressur && ( + + 🏇 Dressur + + )} + {sparteSpringen && ( + + 🐮 Springen + + )} + {klasseC && ( + + Klasse C + + )} + {klasseB && ( + + Klasse B + + )} + {klasseA && ( + + Klasse A + + )} + + + {/* Details */} + + {(datumVon || datumBis) && ( + + 📅 {datumVon?.toLocaleDateString('de-DE') || '...'} - {datumBis?.toLocaleDateString('de-DE') || '...'} + + )} + {verein && ( + + đŸ›ïž {verein} + + )} + + + {/* Kommentar */} + {kommentar && ( + + {kommentar} + + )} + + + )} + + {/* Formular (volle Breite) */} + + {/* Hinweis fĂŒr neue Veranstaltung */} + {istNeu && !istInitialisiert && ( + + + 🔑 Turnier-Nummer erforderlich + + + Bitte geben Sie zuerst eine 5-stellige Turnier-Nummer ein und klicken Sie auf + "Initialisieren". + Diese eindeutige Nummer wird vom ÖPSS vergeben und dient als SchlĂŒssel fĂŒr die + Datenbank-Initialisierung. + + + )} + + {/* Turniernummer mit Initialisieren-Button */} + + + Turnier-Nr.: {istNeu && !istInitialisiert && *} + + + { + const value = e.target.value; + // Nur Zahlen erlauben, maximal 5 Stellen + if (value === '' || (/^\d+$/.test(value) && value.length <= 5)) { + setTurniernummer(value); + validateTurniernummer(value); + } + }} + placeholder="z.B. 26128" + autoFocus={istNeu} + disabled={istInitialisiert && istNeu} + error={!!turniernummerError} + helperText={turniernummerError} + sx={{ + width: 150, + '& .MuiInputBase-input': { + fontSize: '11px', + py: 0.75, + fontWeight: istNeu && !istInitialisiert ? 600 : 400, + }, + '& .MuiOutlinedInput-root': { + bgcolor: istNeu && !istInitialisiert ? 'info.50' : 'background.paper' + } + }} + /> + {istNeu && !istInitialisiert && ( + + )} + + + + {/* Typ */} + + + Typ: + + + setTyp(e.target.value)} + > + } + label={National} + /> + } + label={International} + disabled + /> + + + + (kommt spĂ€ter) + + + + {/* Sprache */} + + + Sprache: + + + setSprache(e.target.value)} + > + } + label={Deutsch} + /> + } + label={English} + disabled + /> + + + + (kommt spĂ€ter) + + + + {/* Sparten */} + + + Sparten: + + + { + setSparteDressur(e.target.checked); + setSelectedKategorien([]); + }} + disabled={!istInitialisiert} + /> + } + label={Dressur} + /> + { + setSparteSpringen(e.target.checked); + setSelectedKategorien([]); + }} + disabled={!istInitialisiert} + /> + } + label={Springen} + /> + + + + {/* Klassen */} + + + Klassen: + + + { + setKlasseC(e.target.checked); + setSelectedKategorien([]); + }} + disabled={!istInitialisiert} + /> + } + label={C} + /> + { + setKlasseB(e.target.checked); + setSelectedKategorien([]); + }} + disabled={!istInitialisiert} + /> + } + label={B} + /> + { + setKlasseA(e.target.checked); + setSelectedKategorien([]); + }} + disabled={!istInitialisiert} + /> + } + label={A} + /> + + + + {/* Kategorien (Mehrfachauswahl) */} + + + Kategorien: + + + {verfuegbareKategorien.length > 0 ? ( + + {verfuegbareKategorien.map((kategorie) => ( + handleKategorieToggle(kategorie)} + disabled={!istInitialisiert} + /> + } + 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 von und bis in einer Zeile */} + + + Datum: + + + setDatumVon(newValue)} + disabled={!istInitialisiert} + slotProps={{ + textField: { + size: 'small', + placeholder: 'von', + sx: {width: 160, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}} + } + }} + /> + bis + setDatumBis(newValue)} + disabled={!istInitialisiert} + minDate={datumVon || undefined} + slotProps={{ + textField: { + size: 'small', + placeholder: 'bis', + sx: {width: 160, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}} + } + }} + /> + + + + {/* Verein */} + + + Verein: + + setVerein(newValue)} + disabled={!istInitialisiert} + options={mockVereine} + renderInput={(params) => ( + + )} + /> + + + {/* Logo */} + + + Logo: + + setLogo(e.target.value)} + disabled={!istInitialisiert} + placeholder="Logo-Datei auswĂ€hlen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + + + {/* Turnier-Titel */} + + + Turnier-Titel: + + setTurnierTitel(e.target.value)} + disabled={!istInitialisiert} + placeholder="FrĂŒhjahrs-Turnier 2026" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + + {/* Kommentar */} + + + Kommentar: + + setKommentar(e.target.value)} + disabled={!istInitialisiert} + placeholder="z.B. KIDS CUP ‱ PONY EINSTEIGER CUP OÖ" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + /> + + + {/* Action Buttons */} + + + + + + + {/* BestĂ€tigungsdialog fĂŒr Initialisierung */} + setShowInitDialog(false)} + maxWidth="sm" + fullWidth + > + + Turnier-Nummer bestĂ€tigen + + + + + ⚠ Wichtig + + + Die Turnier-Nummer kann nach der Initialisierung nicht mehr geĂ€ndert werden. + + + Bitte ĂŒberprĂŒfen Sie die eingegebene Nummer sorgfĂ€ltig. + + + + + + Turnier-Nr.: + + + {turniernummer} + + + + + Ist diese Turnier-Nummer korrekt? + + + + + + + + + + ); +} diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/TransferTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/TransferTab.tsx new file mode 100644 index 00000000..eb751831 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/VeranstaltungUebersicht.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/turnier/VeranstaltungUebersicht.tsx new file mode 100644 index 00000000..5530439d --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/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/Vison_01/src/app/components/ui/accordion.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/accordion.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/accordion.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/accordion.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/alert-dialog.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/alert-dialog.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/alert-dialog.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/alert-dialog.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/alert.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/alert.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/alert.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/alert.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/aspect-ratio.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/aspect-ratio.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/aspect-ratio.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/aspect-ratio.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/avatar.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/avatar.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/avatar.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/avatar.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/badge.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/badge.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/badge.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/badge.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/breadcrumb.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/breadcrumb.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/breadcrumb.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/breadcrumb.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/button.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/button.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/button.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/button.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/calendar.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/calendar.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/calendar.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/calendar.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/card.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/card.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/card.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/card.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/carousel.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/carousel.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/carousel.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/carousel.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/chart.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/chart.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/chart.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/chart.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/checkbox.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/checkbox.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/checkbox.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/checkbox.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/collapsible.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/collapsible.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/collapsible.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/collapsible.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/command.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/command.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/command.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/command.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/context-menu.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/context-menu.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/context-menu.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/context-menu.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/dialog.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/dialog.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/dialog.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/dialog.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/drawer.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/drawer.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/drawer.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/drawer.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/dropdown-menu.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/dropdown-menu.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/dropdown-menu.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/dropdown-menu.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/form.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/form.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/form.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/form.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/hover-card.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/hover-card.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/hover-card.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/hover-card.tsx diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/input-otp.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/input-otp.tsx new file mode 100644 index 00000000..d1da7d25 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/input-otp.tsx @@ -0,0 +1,77 @@ +"use client"; + +import * as React from "react"; +import {OTPInput, OTPInputContext} from "input-otp"; +import {MinusIcon} from "lucide-react"; + +import {cn} from "./utils"; + +function InputOTP({ + className, + containerClassName, + ...props + }: React.ComponentProps & { + containerClassName?: string; +}) { + return ( + + ); +} + +function InputOTPGroup({className, ...props}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function InputOTPSlot({ + index, + className, + ...props + }: React.ComponentProps<"div"> & { + index: number; +}) { + const inputOTPContext = React.useContext(OTPInputContext); + const {char, hasFakeCaret, isActive} = inputOTPContext?.slots[index] ?? {}; + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ); +} + +function InputOTPSeparator({...props}: React.ComponentProps<"div">) { + return ( +
+ +
+ ); +} + +export {InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator}; diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/input.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/input.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/input.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/input.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/label.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/label.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/label.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/label.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/menubar.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/menubar.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/menubar.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/menubar.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/navigation-menu.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/navigation-menu.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/navigation-menu.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/navigation-menu.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/pagination.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/pagination.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/pagination.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/pagination.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/popover.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/popover.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/popover.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/popover.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/progress.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/progress.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/progress.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/progress.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/radio-group.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/radio-group.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/radio-group.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/radio-group.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/resizable.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/resizable.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/resizable.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/resizable.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/scroll-area.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/scroll-area.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/scroll-area.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/scroll-area.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/select.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/select.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/select.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/select.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/separator.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/separator.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/separator.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/separator.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/sheet.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/sheet.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/sheet.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/sheet.tsx diff --git a/docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/sidebar.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/sidebar.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vison_01/src/app/components/ui/sidebar.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/sidebar.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/skeleton.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/skeleton.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/skeleton.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/skeleton.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/slider.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/slider.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/slider.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/slider.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/sonner.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/sonner.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/sonner.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/sonner.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/switch.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/switch.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/switch.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/switch.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/table.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/table.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/table.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/table.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/tabs.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/tabs.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/tabs.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/tabs.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/textarea.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/textarea.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/textarea.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/textarea.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/toggle-group.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/toggle-group.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/toggle-group.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/toggle-group.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/toggle.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/toggle.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/toggle.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/toggle.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/tooltip.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/tooltip.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/tooltip.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/tooltip.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/use-mobile.ts b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/use-mobile.ts similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/use-mobile.ts rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/use-mobile.ts diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/utils.ts b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/utils.ts similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/utils.ts rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/components/ui/utils.ts diff --git a/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/routes.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/routes.tsx new file mode 100644 index 00000000..73573a35 --- /dev/null +++ b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/routes.tsx @@ -0,0 +1,28 @@ +import {createBrowserRouter} from 'react-router'; +import {Login} from './components/Login'; +import {AdminVerwaltung} from './components/Dashboard'; +import {TurnierErstellen} from './components/TurnierErstellen'; +import {TurnierAnsicht} from './components/TurnierAnsicht'; + +export const router = createBrowserRouter([ + { + path: '/', + Component: Login, + }, + { + path: '/admin', + Component: AdminVerwaltung, + }, + { + path: '/veranstaltung/:id', + Component: TurnierErstellen, + }, + { + path: '/veranstaltung/:veranstaltungId/turnier/neu', + Component: TurnierAnsicht, + }, + { + path: '/veranstaltung/:veranstaltungId/turnier/:nr', + Component: TurnierAnsicht, + }, +]); diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/theme.tsx b/docs/03_Development/Frontend/FIGMA/Vision_02/src/app/theme.tsx similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/app/theme.tsx rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/app/theme.tsx diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/imports/26128.md b/docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/26128.md similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/imports/26128.md rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/26128.md diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/imports/26129.md b/docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/26129.md similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/imports/26129.md rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/26129.md diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/imports/Detail-Bewerbe-Springen-Dressur.md b/docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/Detail-Bewerbe-Springen-Dressur.md similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/imports/Detail-Bewerbe-Springen-Dressur.md rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/Detail-Bewerbe-Springen-Dressur.md diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/imports/pasted_text/meldestelle-desktop-screens.md b/docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/pasted_text/meldestelle-desktop-screens.md similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/imports/pasted_text/meldestelle-desktop-screens.md rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/pasted_text/meldestelle-desktop-screens.md diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/imports/pasted_text/nennungs-maske-design.md b/docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/pasted_text/nennungs-maske-design.md similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/imports/pasted_text/nennungs-maske-design.md rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/imports/pasted_text/nennungs-maske-design.md diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/styles/fonts.css b/docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/fonts.css similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/styles/fonts.css rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/fonts.css diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/styles/index.css b/docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/index.css similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/styles/index.css rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/index.css diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/styles/tailwind.css b/docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/tailwind.css similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/styles/tailwind.css rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/tailwind.css diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/styles/theme.css b/docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/theme.css similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/src/styles/theme.css rename to docs/03_Development/Frontend/FIGMA/Vision_02/src/styles/theme.css diff --git a/docs/06_Frontend/FIGMA/Vision_03/vite.config.ts b/docs/03_Development/Frontend/FIGMA/Vision_02/vite.config.ts similarity index 100% rename from docs/06_Frontend/FIGMA/Vision_03/vite.config.ts rename to docs/03_Development/Frontend/FIGMA/Vision_02/vite.config.ts diff --git a/docs/03_Development/Frontend/FIGMA/Vision_03/ATTRIBUTIONS.md b/docs/03_Development/Frontend/FIGMA/Vision_03/ATTRIBUTIONS.md new file mode 100644 index 00000000..ce6bb5a6 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/NAVIGATION.md b/docs/03_Development/Frontend/FIGMA/Vision_03/NAVIGATION.md new file mode 100644 index 00000000..c338bdf8 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/README.md b/docs/03_Development/Frontend/FIGMA/Vision_03/README.md new file mode 100644 index 00000000..f3fb1fab --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/docs/README.md b/docs/03_Development/Frontend/FIGMA/Vision_03/docs/README.md new file mode 100644 index 00000000..636fe7a2 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/docs/UI-UX.md b/docs/03_Development/Frontend/FIGMA/Vision_03/docs/UI-UX.md new file mode 100644 index 00000000..d0ecf97f --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/guidelines.zip b/docs/03_Development/Frontend/FIGMA/Vision_03/guidelines.zip new file mode 100644 index 00000000..28fae346 Binary files /dev/null and b/docs/03_Development/Frontend/FIGMA/Vision_03/guidelines.zip differ diff --git a/docs/03_Development/Frontend/FIGMA/Vision_03/package.json b/docs/03_Development/Frontend/FIGMA/Vision_03/package.json new file mode 100644 index 00000000..83bc21b3 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/postcss.config.mjs b/docs/03_Development/Frontend/FIGMA/Vision_03/postcss.config.mjs new file mode 100644 index 00000000..531dbecd --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src.zip b/docs/03_Development/Frontend/FIGMA/Vision_03/src.zip new file mode 100644 index 00000000..dfd3b136 Binary files /dev/null and b/docs/03_Development/Frontend/FIGMA/Vision_03/src.zip differ diff --git a/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/App.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/App.tsx new file mode 100644 index 00000000..04cd85c6 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/Bewerbsliste.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/Bewerbsliste.tsx new file mode 100644 index 00000000..d0ce73b2 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/Dashboard.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/Dashboard.tsx new file mode 100644 index 00000000..08189f04 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/Login.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/Login.tsx new file mode 100644 index 00000000..225a5f63 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/NennungenTabelle.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/NennungenTabelle.tsx new file mode 100644 index 00000000..1af2c8c8 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/NennungsMaske.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/NennungsMaske.tsx new file mode 100644 index 00000000..bdee0109 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/NeuerVeranstalter.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/NeuerVeranstalter.tsx new file mode 100644 index 00000000..764fbeeb --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/PferdReiterEingabe.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/PferdReiterEingabe.tsx new file mode 100644 index 00000000..42488218 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/TurnierAnsicht.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/TurnierAnsicht.tsx new file mode 100644 index 00000000..b154f88b --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/TurnierErstellen.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/TurnierErstellen.tsx new file mode 100644 index 00000000..e0c7b0d9 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VeranstalterAuswahl.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VeranstalterAuswahl.tsx new file mode 100644 index 00000000..9054556c --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VeranstalterProfil.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VeranstalterProfil.tsx new file mode 100644 index 00000000..b347f540 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VeranstalterUebersicht.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VeranstalterUebersicht.tsx new file mode 100644 index 00000000..aac7ff4d --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VerkaufBuchungen.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/VerkaufBuchungen.tsx new file mode 100644 index 00000000..8fad1549 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/figma/ImageWithFallback.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/figma/ImageWithFallback.tsx new file mode 100644 index 00000000..ff6e48f5 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/AbrechnungTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/AbrechnungTab.tsx new file mode 100644 index 00000000..2756f68f --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/ArtikelTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/ArtikelTab.tsx new file mode 100644 index 00000000..ff7f42d0 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/BewerbeTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/BewerbeTab.tsx new file mode 100644 index 00000000..469d1918 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/ErgebnislistenTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/ErgebnislistenTab.tsx new file mode 100644 index 00000000..aad40d3b --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/FunktionaereTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/FunktionaereTab.tsx new file mode 100644 index 00000000..08626363 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/NennungenTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/NennungenTab.tsx new file mode 100644 index 00000000..f65ee574 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/OrganisationTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/OrganisationTab.tsx new file mode 100644 index 00000000..7158b8e0 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/PreislisteTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/PreislisteTab.tsx new file mode 100644 index 00000000..c6fec89d --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/StammdatenTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/StammdatenTab.tsx new file mode 100644 index 00000000..72845788 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/StartlistenTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/StartlistenTab.tsx new file mode 100644 index 00000000..073c3bea --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/TransferTab.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/TransferTab.tsx new file mode 100644 index 00000000..eb751831 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/VeranstaltungUebersicht.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/turnier/VeranstaltungUebersicht.tsx new file mode 100644 index 00000000..5530439d --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/accordion.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/accordion.tsx new file mode 100644 index 00000000..19e8905a --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/alert-dialog.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..d49018d1 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/alert.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/alert.tsx new file mode 100644 index 00000000..6424cc40 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/aspect-ratio.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..cd697698 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/avatar.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/avatar.tsx new file mode 100644 index 00000000..cac4642f --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/badge.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/badge.tsx new file mode 100644 index 00000000..07ffa941 --- /dev/null +++ b/docs/03_Development/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/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/breadcrumb.tsx b/docs/03_Development/Frontend/FIGMA/Vision_03/src/app/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..6916fcd1 --- /dev/null +++ b/docs/03_Development/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