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