docs: add event details and session log for Neumarkt 2026 tournaments
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 7m2s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m46s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 2m57s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m49s

- Added competition details for Neumarkt tournaments 26128 and 26129 under `docs/Neumarkt2026/`.
- Logged key outcomes of the Domain Workshop follow-up and Frontend Kick-off session under `docs/99_Journal/2026-03-18_Session_Log_Domain_und_Frontend_Kickoff.md`.
- Updated `frontend/shells/meldestelle-portal` with new routing and UI components for Landing Page, Dashboard, and Tournament creation flow.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-18 14:14:38 +01:00
parent 2538be395a
commit 32295bdea2
11 changed files with 810 additions and 93 deletions
@@ -0,0 +1,55 @@
# Session Log: 18.03.2026 - Domain Workshop (Fortsetzung) & Frontend Kick-off
**Teilnehmer:** Owner, 🏗️ [Lead Architect], 📜 [ÖTO/FEI Rulebook Expert], 👷 [Backend Developer], 🎨 [Frontend Expert],
🖌️ [UI/UX Designer], 🧹 [Curator]
## Ziel der Session
Zusammenfassung der Domain-Erkenntnisse vom Vortag, Festlegung des Scopes für das MVP (Phase 1) und Start der
Frontend-Implementierung auf Basis des "UI-Driven Development" Ansatzes.
## Ergebnisse & Beschlüsse
### 1. Scope für MVP (Phase 1)
* **Fokus-Turniere:** Die Turniere in Neumarkt (26128 und 26129) Ende April dienen als erstes großes Etappenziel (MVP
Feldtest).
* **Turnier-Kategorien:** Fokus ausschließlich auf **C-NEU** und **C**.
* **Sparten:** Fokus ausschließlich auf **Dressur (D)** und **Springen (S)**.
* **Begründung:** Diese Eingrenzung reduziert die Komplexität des Regelwerks massiv und ermöglicht eine schnelle
Auslieferung eines produktionsreifen Kernsystems für den Großteil der nationalen Turniere.
### 2. Frontend-Entwicklung (UI-Driven Development)
* Die Entwicklung startet beim Frontend (`frontend/shells/meldestelle-portal`), um den perfekten, praxisnahen
Workflow ("Enter & Tab") zu garantieren, bevor das Backend die Daten liefert.
* **Landing Page (`mo-code.at`):** Wurde neu designt. Kern-Säulen des Systems (Regelwerks-Intelligenz, Offline-First,
Speed-Workflow, Smarte Kassenführung) wurden als USPs platziert.
* **Aktuelle Turniere:** Eine neue Sektion auf der Landing Page zeigt eine Liste der kommenden Turniere (mit
Platzhaltern für Neumarkt 26128/26129).
* **Meldestelle Dashboard:** Nach dem Login gelangt der User nicht mehr auf eine generische "Welcome"-Seite, sondern in
ein dediziertes Dashboard.
* Links: Verwaltung der eigenen Turniere (inkl. Absprung ins Meldestellen-Cockpit).
* Rechts: System-Tools (Ping-Service, ZNS-Import).
* **Turnieranlage Wizard:** Der Button "+ Neues Turnier anlegen" führt nun in einen mehrstufigen Wizard:
1. Stammdaten & ZNS Import
2. Konfiguration (Austragungsplätze)
3. Funktionäre
4. Bewerbe anlegen (Master-Detail Ansicht analog zum Legacy-System)
* **Korrektur Compose-Updates:** Veraltete Compose-Komponenten (`Divider`, `TabRow`) wurden durch ihre modernen
Pendants (`HorizontalDivider`, `PrimaryTabRow`) ersetzt, um Kompilierfehler zu beheben.
### 3. Nächste Schritte (Backlog)
* **Backend:** Implementierung des ZNS-Importers (Entpacken der `zns.zip`, Einlesen der `.dat` Dateien mit Codepage 850,
Generierung erster Events).
* **Frontend:** Detaillierung der "Nennungs-Maske" (Cockpit), die für die schnelle telefonische Nennung optimiert werden
muss.
## Aktualisierte Dokumente
* `docs/03_Domain/01_Core_Model/Entities/Event_Structure_Diagram.md` (Scope-Eingrenzung auf C-NEU/C & D/S dokumentiert)
* `docs/03_Domain/03_Analysis/Domain_Workshop_Results_2026-03-17.md` (MVP-Fokus hinzugefügt)
* `frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt` (Landing Page, Dashboard & Wizard implementiert)
* `frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt` (Routen für
Dashboard & CreateTournament hinzugefügt)
+71
View File
@@ -0,0 +1,71 @@
# CSN-C NEU / CSNP-C NEU NEUMARKT/M.
**Turnier-Nr.: 26128** | [cite_start]**Datum: 25. April 2026** [cite: 1, 2]
## Allgemeine Informationen
* **Veranstalter:** Union Reit- u. Fahrverein Neumarkt/M. (6-009) [cite_start][cite: 3]
* [cite_start]**Ort:** Reitanlage Stroblmair, 4212 Neumarkt [cite: 3]
* [cite_start]**Kontakt:** Ursula Stroblmair, Brandstetterweg 2, 4212 Neumarkt [cite: 4]
* **Tel.:** 0664 1832381
* [cite_start]**E-Mail:** reit-stall@gmx.at [cite: 4]
* [cite_start]**Nennungsschluss:** 24.04.2026, 19:00 Uhr [cite: 4]
* [cite_start]**Online-Nennung:** Ab Mittwoch, 22.04.
auf [www.ihremeldestelle.at](http://www.ihremeldestelle.at) [cite: 5]
* [cite_start]**Meldestelle:** Geöffnet ab 24.04., 17:00 Uhr (Tel: +43 681 10769120) [cite: 8]
## Technische Details
* [cite_start]**Austragungsplatz:** 45 x 65 m (Sand/Vlies) [cite: 6]
* [cite_start]**Vorbereitungsplatz:** 20 x 40 m Halle (Sand/Vlies) [cite: 6]
* [cite_start]**Warmreiten:** Draußen (20 x 60 m Sand/Vlies) möglich [cite: 16]
* [cite_start]**Boxen:** Keine Einstallung möglich [cite: 9]
## Funktionäre
* [cite_start]**Turnierleiter:** Ursula Stroblmair [cite: 6]
* [cite_start]**Turnierbeauftragter:** Rudi Kreupl [cite: 7]
* [cite_start]**Richter:** Rudi Kreupl, Helmut Riedler [cite: 7]
* [cite_start]**Parcoursbauchef:** Kurt Reitetschlägerr [cite: 8]
* [cite_start]**Tierarzt:** Dr. Sabine Ötschmaier [cite: 8]
---
## Besondere Bestimmungen
* **Kosten:** Startgeld € 15,- pro Bewerb. [cite_start]Kein Nenngeld, kein Sporteuro. [cite: 11]
* **Teilnahmebedingungen:**
* [cite_start]Für Springprüfungen bis 95 cm: Mitgliedschaft OEPS-Verein und Reiterpass erforderlich. [cite: 12]
* [cite_start]Pferde bis 90 cm müssen **nicht** beim OEPS registriert sein. [cite: 14]
* [cite_start]Pferdepass mit gültigem Impfschutz (§ 11 OTO) ist vorzulegen. [cite: 15]
* [cite_start]Haftpflichtversicherung für jedes Pferd ist Pflicht. [cite: 21]
* **Startregelung:**
* [cite_start]Ein Pferd darf maximal 3x pro Tag starten. [cite: 14]
* [cite_start]In Bewerben bis 95 cm darf ein Pferd mit zwei verschiedenen Reitern starten. [cite: 13]
* [cite_start]**Hunde:** Am gesamten Gelände herrscht Leinenpflicht. [cite: 18]
---
## Bewerbe (Samstag, 25. April 2026 - Beginn 08:00 Uhr)
| Nr. | Bewerb | Höhe | Richtverfahren / Abteilungen |
|:-------|:--------------------------------|:-------|:------------------------------------------------------------------------------------|
| **1** | Pony Stilspringprüfung | 60 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
| **2** | Einlaufspringprüfung | 60 cm | [cite_start]RV: § 204/4 (1. Abt: lizenzfrei / 2. Abt: mit Lizenz) [cite: 27] |
| **3** | Pony Stilspringprüfung | 70 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
| **4** | Einlaufspringprüfung | 70 cm | [cite_start]RV: § 218 (1. Abt: lizenzfrei / 2. Abt: mit Lizenz) [cite: 27] |
| **5** | Pony Stilspringprüfung | 80 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
| **6** | Stilspringprüfung | 80 cm | [cite_start]RV: § 204/4 (1. Abt: lizenzfrei / 2. Abt: R1 & 5-6j. Pferde) [cite: 27] |
| **7** | Pony Stilspringprüfung | 95 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
| **8** | Springreiterbewerb (lizenzfrei) | 95 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
| **9** | Standardspringprüfung | 95 cm | [cite_start]RV: A2 (1. Abt: R1 / 2. Abt: R2 und höher) [cite: 27] |
| **10** | Springpferdeprüfung | 105 cm | [cite_start]RV: § 203/3 (1. Abt: 4-jährig / 2. Abt: 5-6-jährig) [cite: 27] |
| **11** | Stilspringprüfung | 105 cm | [cite_start]RV: § 204/4 (1. Abt: R1) [cite: 27] |
| **12** | Standardspringprüfung | 105 cm | [cite_start]RV: A2 (1. Abt: R1 / 2. Abt: R2/RS2 und höher) [cite: 27] |
| **13** | Stilspringprüfung | 115 cm | [cite_start]RV: § 204/4 (1. Abt: R1) [cite: 28, 30, 31] |
| **14** | Standardspringprüfung | 115 cm | [cite_start]RV: A2 (1. Abt: R1 / 2. Abt: R2/RS2 und höher) [cite: 32, 34, 36] |
---
**Haftung:** Der Veranstalter übernimmt keine Haftung. [cite_start]Teilnehmer haften persönlich für Schäden gegenüber
Dritten. [cite: 19, 20]
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
# CDN-C NEU / CDNP-C NEU NEUMARKT/M., OÖ
**Turnier-Nr.: 26129** | [cite_start]**Datum: 26. April 2026** [cite: 37]
## Allgemeine Informationen
* **Veranstalter**: Union Reit- u. Fahrverein Neumarkt/M. (6-009) [cite_start][cite: 38].
* [cite_start]**Ort**: Reitanlage Stroblmair, 4212 Neumarkt[cite: 38].
* [cite_start]**Kontaktadresse**: Ursula Stroblmair, Brandstetterweg 2, 4212 Neumarkt[cite: 39].
* [cite_start]**Telefon**: 0664 1832381[cite: 39].
* [cite_start]**E-Mail**: reit-stall@gmx.at[cite: 39].
* [cite_start]**Nennungsschluss**: 25.04.2026, 19:00 Uhr[cite: 39, 53].
* [cite_start]**Online-Nennung**: Ab Mittwoch, 22.04. auf www.ihremeldestelle.at möglich[cite: 40].
* [cite_start]**Meldestelle**: Geöffnet ab 25.04., 17:00 Uhr (Tel: +43 681 10769120)[cite: 43].
* [cite_start]**Start- und Ergebnislisten**: Ab 20:30 Uhr auf www.ihremeldestelle.at verfügbar[cite: 44].
## Technische Details und Gebühren
* [cite_start]**Austragungsplatz**: 20 x 60 m Sand/Vlies[cite: 41].
* [cite_start]**Vorbereitungsplatz**: 20 x 40 m Halle (Sand/Vlies) und 20 x 60 m (Sand/Vlies)[cite: 41].
* [cite_start]**Boxen**: Keine Einstallung möglich[cite: 44].
* [cite_start]**Kosten**: Startgeld € 15,- pro Bewerb; kein Nenngeld und kein Sporteuro[cite: 40, 47].
## Funktionäre
* [cite_start]**Turnierleiter**: Ursula Stroblmair[cite: 41].
* [cite_start]**Turnierbeauftragte**: Alexandra Schuster[cite: 42].
* [cite_start]**Richter**: Alexandra Schuster, Ulrike Knasmüller-Prinz, Karin Wallner[cite: 42].
* [cite_start]**Steward**: Barbara Hruschka[cite: 42].
* [cite_start]**Tierarzt**: Dr. Sabine Ötschmaier[cite: 42].
---
## Besondere Bestimmungen
* **Teilnahmevoraussetzungen**:
* [cite_start]Für Reiterpass-/Reiternadel-Aufgaben ist die Mitgliedschaft bei einem OEPS-Verein und der Besitz des
Reiterpasses erforderlich[cite: 48].
* [cite_start]Pferde für Reiterpass-/Reiternadel-Aufgaben müssen nicht beim OEPS registriert sein[cite: 50].
* **Pferde**:
* [cite_start]Ein Pferd darf pro Tag maximal 3x starten[cite: 49].
* [cite_start]Ein Pferd darf mit zwei verschiedenen Reitern an den Start gehen[cite: 49].
* [cite_start]Vorlage des Pferdepasses mit gültigem Impfschutz gemäß § 11 OTO ist Pflicht[cite: 51].
* [cite_start]Jedes teilnehmende Pferd muss haftpflichtversichert sein[cite: 57].
* [cite_start]**Haftung**: Der Veranstalter übernimmt keine Haftung jeder Art und Ursache[cite: 55]. [cite_start]
Teilnehmer und Besitzer haften persönlich für Schäden gegenüber Dritten[cite: 56].
* [cite_start]**Sonstiges**: Es gilt Leinenpflicht für Hunde auf dem gesamten Gelände[cite: 54]. [cite_start]
Ausländische Equiden unterliegen der TRACES-Pflicht[cite: 58].
---
## Bewerbe (Sonntag, 26. April 2026 - Beginn 08:00 Uhr)
| Nr. | Bewerb | Aufg. | Details / Abteilungen |
|:-------|:---------------------------------|:---------------|:------------------------------------------------------------------------|
| **1** | Dressurreiterprüfung Reiterpass | R1 | [cite_start]RV: A § 103/5 [cite: 63] |
| **2** | Dressurreiterprüfung Reiternadel | R4 | [cite_start]RV: A § 103/5 [cite: 64] |
| **3** | Dressurreiterprüfung lizenzfrei | LF1 | [cite_start]RV: A § 103/5 [cite: 68] |
| **4** | Dressurreiterprüfung lizenzfrei | LF3 | [cite_start]RV: A § 103/5 [cite: 69] |
| **5** | First Ridden | - [cite_start] | [cite: 71] |
| **6** | Führzügelklasse | - [cite_start] | [cite: 73] |
| **7** | Pony Dressurprüfung Kl. A | P1 | [cite_start]RV: A, § 901 [cite: 75, 76] |
| **8** | Dressurreiterprüfung Kl. A | DRA1 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 78, 79, 81] |
| **9** | Dressurprüfung Kl. A | A5 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 82, 83, 98] |
| **13** | Dressurpferdeprüfung Kl. A | DPA1 | 1. Abt: 4-jähr. Pferde; 2. Abt: 5-6-jähr. [cite_start]Pferde [cite: 85] |
| **14** | Dressurpferdprüfung Kl. L | DPL1 | Für 5-6-jähr. [cite_start]Pferde [cite: 87] |
| **10** | Pony Dressurprüfung Kl. L | P6 | [cite_start]RV: A, § 901 [cite: 89, 90] |
| **11** | Dressurreiterprüfung Kl. L | DRL1 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 89, 92, 97] |
| **12** | Dressurprüfung Kl. L | L3 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 94, 96] |
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.
@@ -3,6 +3,8 @@ package at.mocode.frontend.core.navigation
sealed class AppScreen(val route: String) {
data object Landing : AppScreen(Routes.HOME)
data object Home : AppScreen("/home")
data object Dashboard : AppScreen("/dashboard")
data object CreateTournament : AppScreen("/tournament/create") // Neuer Screen
data object Login : AppScreen(Routes.LOGIN)
data object Ping : AppScreen("/ping")
data object Profile : AppScreen("/profile")
@@ -13,6 +15,8 @@ sealed class AppScreen(val route: String) {
return when (route) {
Routes.HOME -> Landing
"/home" -> Home
"/dashboard" -> Dashboard
"/tournament/create" -> CreateTournament
Routes.LOGIN, Routes.Auth.LOGIN -> Login
"/ping" -> Ping
"/profile" -> Profile
@@ -1,19 +1,20 @@
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.collectAsState
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.auth.presentation.LoginScreen
import at.mocode.frontend.core.auth.presentation.LoginViewModel
import at.mocode.ping.feature.presentation.PingScreen
import at.mocode.ping.feature.presentation.PingViewModel
import at.mocode.frontend.core.designsystem.components.AppFooter
import at.mocode.frontend.core.designsystem.theme.AppTheme
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.ping.feature.presentation.PingScreen
import at.mocode.ping.feature.presentation.PingViewModel
import navigation.StateNavigationPort
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -39,31 +40,48 @@ fun MainApp() {
when (currentScreen) {
is AppScreen.Landing -> LandingScreen(
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login) },
onSecondary = { navigationPort.navigateToScreen(AppScreen.Home) }
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login) }
)
is AppScreen.Home -> WelcomeScreen(
is AppScreen.Dashboard -> DashboardScreen(
authTokenManager = authTokenManager,
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
onOpenLogin = { navigationPort.navigateToScreen(AppScreen.Login) },
onOpenProfile = { navigationPort.navigateToScreen(AppScreen.Profile) }
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) },
onLogout = {
authTokenManager.clearToken()
navigationPort.navigateToScreen(AppScreen.Landing)
}
)
is AppScreen.Home -> DashboardScreen( // Route /home to Dashboard for now
authTokenManager = authTokenManager,
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) },
onLogout = {
authTokenManager.clearToken()
navigationPort.navigateToScreen(AppScreen.Landing)
}
)
is AppScreen.CreateTournament -> CreateTournamentScreen(
onBack = { navigationPort.navigateToScreen(AppScreen.Dashboard) },
onSave = { navigationPort.navigateToScreen(AppScreen.Dashboard) } // Later we go to tournament detail
)
is AppScreen.Login -> LoginScreen(
viewModel = loginViewModel,
onLoginSuccess = { navigationPort.navigateToScreen(AppScreen.Profile) },
onBack = { navigationPort.navigateToScreen(AppScreen.Home) }
onLoginSuccess = { navigationPort.navigateToScreen(AppScreen.Dashboard) },
onBack = { navigationPort.navigateToScreen(AppScreen.Landing) }
)
is AppScreen.Ping -> PingScreen(
viewModel = pingViewModel,
onBack = { navigationPort.navigateToScreen(AppScreen.Home) } // Navigate back to Home
onBack = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
)
is AppScreen.Profile -> AuthStatusScreen(
authTokenManager = authTokenManager,
onBackToHome = { navigationPort.navigateToScreen(AppScreen.Home) }
onBackToHome = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
)
else -> {}
@@ -74,8 +92,7 @@ fun MainApp() {
@Composable
private fun LandingScreen(
onPrimaryCta: () -> Unit,
onSecondary: () -> Unit
onPrimaryCta: () -> Unit
) {
val scrollState = rememberScrollState()
@@ -84,56 +101,103 @@ private fun LandingScreen(
.fillMaxSize()
.verticalScroll(scrollState)
) {
// Top Bar area (simple for landing)
Surface(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "mo-code.at",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Button(onClick = onPrimaryCta) {
Text("Login Meldestelle")
}
}
}
// Hero
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(horizontal = 24.dp, vertical = 60.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "EquestEvents",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Die kompetente TurnierMeldestelle.",
style = MaterialTheme.typography.headlineLarge
text = "Die moderne Turniermeldestelle",
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "EquestEvents entwickelt die digitale Infrastruktur des österreichischen Pferdesports aus der Praxis. Für die Praxis.",
style = MaterialTheme.typography.bodyLarge
text = "Von Praktikern für Praktiker. Schneller Nennen, fehlerfrei Richten und stressfrei Auswerten konform nach ÖTO & FEI.",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onPrimaryCta) { Text("Anmelden (PilotPartner)") }
TextButton(onClick = onSecondary) { Text("Mehr erfahren") }
}
}
// Manifest
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
// --- AKTUELLE TURNIERE SECTION ---
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text("Aktuelle Turniere", style = MaterialTheme.typography.headlineMedium)
// Dummy Daten basierend auf Neumarkt 2026
val turniere = listOf(
TournamentData(
id = "26128",
date = "25. APRIL 2026",
title = "CSN-C NEU CSNP-C NEU",
location = "NEUMARKT/M., OÖ"
),
TournamentData(
id = "26129",
date = "26. APRIL 2026",
title = "CDN-C NEU CDNP-C NEU",
location = "NEUMARKT/M., OÖ"
)
)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
turniere.forEach { turnier ->
TournamentCard(turnier)
}
}
}
// Manifest / Intro
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 60.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Unser Anspruch: Durchdachtes System.", style = MaterialTheme.typography.headlineMedium)
Text("Unser Anspruch: Ein durchdachtes System.", style = MaterialTheme.typography.headlineMedium)
Text(
"Die Meldestelle ist das Herzstück jedes Turniers. Wenn sie stolpert, stockt der Sport. Wir verstehen den Balanceakt zwischen Veranstaltern, Reitern und den Verbänden.",
style = MaterialTheme.typography.bodyLarge
)
Text(
"Deshalb entwickeln wir EquestEvents nicht am Reißbrett, sondern direkt am Turnier aus der Sicht der Meldestelle, der Richter, der Zeitnehmer und aller Funktionäre.",
"Deshalb entwickeln wir diese Plattform nicht am Reißbrett, sondern direkt am Turnierplatz aus der Sicht der Meldestelle, der Richter, der Zeitnehmer und aller Funktionäre.",
style = MaterialTheme.typography.bodyLarge
)
Text(
"Aktuell befindet sich unser System in einer Pilotphase für C und CNEUTurniere. Wir wachsen organisch Seite an Seite mit unseren PilotPartnern.",
style = MaterialTheme.typography.bodyLarge
)
Text(
"Jedes Feedback fließt direkt in die Entwicklung ein, um eine Lösung zu schaffen, die den realen Bedürfnissen vor Ort entspricht.",
"Mit Fokus auf die Praxis: Tastaturbedienung für höchste Geschwindigkeit, Offline-Fähigkeit für das 'Plumpsklo' am Rand des Abreiteplatzes und eine integrierte Kassenführung.",
style = MaterialTheme.typography.bodyLarge
)
}
@@ -143,25 +207,30 @@ private fun LandingScreen(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
.padding(horizontal = 24.dp, vertical = 60.dp),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
Text("Die drei Säulen", style = MaterialTheme.typography.headlineMedium)
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Die Kern-Säulen", style = MaterialTheme.typography.headlineMedium)
Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
FeatureCard(
number = "01",
title = "RegelwerksIntelligenz (FEI & ÖTO)",
body = "Wir verbinden ÖTO und FEI und nehmen Ihnen die ValidierungsKomplexität ab. Von der Lizenzprüfung bis zur korrekten Anwendung der Bestimmungen."
title = "Regelwerks-Intelligenz (ÖTO)",
body = "Wir nehmen Ihnen die Validierungs-Komplexität ab. Von der Lizenzprüfung der Reiter bis zur Kontrolle der Richterqualifikationen beim Anlegen der Bewerbe."
)
FeatureCard(
number = "02",
title = "Plattformunabhängig & Offlinefähig",
body = "Stabil auf Laptop und mobil. Dank OfflineUnterstützung arbeiten Sie nahtlos weiter selbst wenn die Internetverbindung am Platz abreißt."
title = "Offline-First & Resilient",
body = "Stabil auf dem Laptop. Dank Offline-Unterstützung und lokaler Datenbank arbeiten Sie nahtlos weiter, selbst wenn die Internetverbindung am Platz wieder einmal abreißt."
)
FeatureCard(
number = "03",
title = "Fokus auf den Sport",
body = "Wir reduzieren Administration dort, wo es sinnvoll ist damit sich alle auf das Wesentliche konzentrieren können: den Reitsport."
title = "Speed-Workflow",
body = "Die Nennungsmaske und die Ergebniserfassung sind kompromisslos auf Geschwindigkeit und Tastaturbedienung (Enter & Tab) optimiert. Weil am Turniertag jede Sekunde zählt."
)
FeatureCard(
number = "04",
title = "Smarte Kassenführung",
body = "Kontobasierte Abrechnung für Reiter und Besitzer. Nenngelder, Startgelder und Nachnenngebühren sauber getrennt selbst ein Nennungstausch wird als einfacher Transfer verbucht."
)
}
}
@@ -171,65 +240,513 @@ private fun LandingScreen(
}
}
// Data class for dummy tournament
private data class TournamentData(
val id: String,
val date: String,
val title: String,
val location: String
)
@Composable
private fun FeatureCard(number: String, title: String, body: String) {
Surface(tonalElevation = 0.dp) {
Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.width(56.dp).padding(top = 6.dp)) {
Text(text = number, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
private fun TournamentCard(data: TournamentData) {
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Left: Logo Placeholder
Surface(
modifier = Modifier.size(100.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
Box(contentAlignment = Alignment.Center) {
Text(
"URFV\nLogo",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.width(24.dp))
// Middle: Info
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = data.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${data.location} ${data.date}",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Turnier-Nr.:${data.id}",
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(modifier = Modifier.width(24.dp))
// Right: Actions
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.width(200.dp)
) {
OutlinedButton(
onClick = { /* TODO */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Ausschreibung")
}
OutlinedButton(
onClick = { /* TODO */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Nennen")
}
OutlinedButton(
onClick = { /* TODO */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Start- Ergebnislisten")
}
Column(modifier = Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(4.dp))
Text(body, style = MaterialTheme.typography.bodyLarge)
}
}
}
}
@Composable
private fun WelcomeScreen(
authTokenManager: AuthTokenManager,
onOpenPing: () -> Unit,
onOpenLogin: () -> Unit,
onOpenProfile: () -> Unit
) {
val authState by authTokenManager.authState.collectAsState()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
private fun FeatureCard(number: String, title: String, body: String) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.Top
) {
Text(
text = "Willkommen zur Meldestelle",
style = MaterialTheme.typography.headlineMedium
text = number,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Black,
modifier = Modifier.width(64.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text(body, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
@Composable
private fun DashboardScreen(
authTokenManager: AuthTokenManager,
onOpenPing: () -> Unit,
onCreateTournament: () -> Unit,
onLogout: () -> Unit
) {
val authState by authTokenManager.authState.collectAsState()
Column(
modifier = Modifier.fillMaxSize()
) {
// App Header (Meldestelle Toolbar)
Surface(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Meldestelle Dashboard",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Angemeldet als: ${authState.username ?: "Admin"}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
TextButton(onClick = onLogout) {
Text("Abmelden")
}
}
}
}
// Main Content Area
Row(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
// Left Column (Turniere)
Column(
modifier = Modifier.weight(2f),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Meine Turniere", style = MaterialTheme.typography.headlineSmall)
// Dummy Turniere für die Meldestelle
val turniere = listOf(
TournamentData(
id = "26128",
date = "25. APRIL 2026",
title = "CSN-C NEU CSNP-C NEU",
location = "NEUMARKT/M., OÖ"
),
TournamentData(
id = "26129",
date = "26. APRIL 2026",
title = "CDN-C NEU CDNP-C NEU",
location = "NEUMARKT/M., OÖ"
)
)
// Auth info
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
if (authState.isAuthenticated) {
Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = onOpenProfile) { Text("Profil anzeigen") }
} else {
Text("Du bist nicht angemeldet.")
turniere.forEach { turnier ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(turnier.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("Nr: ${turnier.id} | ${turnier.date}", style = MaterialTheme.typography.bodyMedium)
}
Button(onClick = { /* TODO: Open Meldestellen Cockpit for this tournament */ }) {
Text("Meldestelle öffnen")
}
}
}
}
// Actions
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onOpenPing, modifier = Modifier.weight(1f)) { Text("Ping-Service") }
if (!authState.isAuthenticated) {
Button(
onClick = onOpenLogin,
OutlinedButton(
onClick = onCreateTournament,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp)
) {
Text("+ Neues Turnier anlegen")
}
}
// Right Column (System / Tools)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("System & Tools", style = MaterialTheme.typography.headlineSmall)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(
onClick = onOpenPing,
modifier = Modifier.fillMaxWidth()
) {
Text("Ping-Service (System Status)")
}
OutlinedButton(
onClick = { /* TODO: ZNS Import */ },
modifier = Modifier.fillMaxWidth()
) {
Text("ZNS-Daten Importieren")
}
}
}
}
}
}
}
@Composable
fun CreateTournamentScreen(
onBack: () -> Unit,
onSave: () -> Unit
) {
// Simple state to track the current step in the wizard
var currentStep by remember { mutableStateOf(1) }
Column(modifier = Modifier.fillMaxSize()) {
// App Header
Surface(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
TextButton(onClick = onBack) {
Text("← Zurück")
}
Text(
text = "Neues Turnier anlegen",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
}
}
// Stepper / Progress Bar
Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StepIndicator(step = 1, title = "Stammdaten", isActive = currentStep == 1, isCompleted = currentStep > 1)
StepIndicator(step = 2, title = "Konfiguration", isActive = currentStep == 2, isCompleted = currentStep > 2)
StepIndicator(step = 3, title = "Funktionäre", isActive = currentStep == 3, isCompleted = currentStep > 3)
StepIndicator(step = 4, title = "Bewerbe", isActive = currentStep == 4, isCompleted = currentStep > 4)
}
}
// Wizard Content Area
Box(modifier = Modifier.weight(1f).padding(24.dp)) {
when (currentStep) {
1 -> TournamentStepStammdaten()
2 -> TournamentStepKonfiguration()
3 -> TournamentStepFunktionaere()
4 -> TournamentStepBewerbe()
}
}
// Bottom Navigation Bar
Surface(shadowElevation = 8.dp, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(24.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
if (currentStep > 1) {
OutlinedButton(onClick = { currentStep-- }) { Text("Zurück") }
} else {
Spacer(modifier = Modifier.width(1.dp)) // Empty space to keep "Weiter" on the right
}
if (currentStep < 4) {
Button(onClick = { currentStep++ }) { Text("Weiter") }
} else {
Button(onClick = onSave) { Text("Turnier speichern") }
}
}
}
}
}
@Composable
fun StepIndicator(step: Int, title: String, isActive: Boolean, isCompleted: Boolean) {
val color = when {
isActive -> MaterialTheme.colorScheme.primary
isCompleted -> MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
}
val fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
shape = MaterialTheme.shapes.small,
color = color,
modifier = Modifier.size(24.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(step.toString(), color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.labelSmall)
}
}
Text(title, color = color, fontWeight = fontWeight)
}
}
@Composable
fun TournamentStepStammdaten() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
Text("Schritt 1: Turnier-Stammdaten", style = MaterialTheme.typography.headlineSmall)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Turniernummer OEPS (z.B. 26128)") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Turniername (z.B. CSN-C NEU Neumarkt)") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Datum von") },
modifier = Modifier.weight(1f)
) { Text("Login") }
)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Datum bis") },
modifier = Modifier.weight(1f)
)
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("ZNS Import", fontWeight = FontWeight.Bold)
Text(
"Hier laden wir später das ZNS.zip für dieses Turnier hoch, um Starter und Lizenzen zu importieren.",
style = MaterialTheme.typography.bodyMedium
)
Button(onClick = { /*TODO*/ }) { Text("ZNS.zip auswählen...") }
}
}
}
}
@Composable
fun TournamentStepKonfiguration() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
Text("Schritt 2: Konfiguration", style = MaterialTheme.typography.headlineSmall)
Text("Austragungsplätze und Preisliste")
// Placeholder for Austragungsplätze
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Austragungsplätze", fontWeight = FontWeight.Bold)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 8.dp)) {
FilterChip(selected = true, onClick = {}, label = { Text("Platz 1 (Sand)") })
FilterChip(selected = false, onClick = {}, label = { Text("Halle") })
FilterChip(selected = false, onClick = {}, label = { Text("+ Hinzufügen") })
}
}
}
}
}
@Composable
fun TournamentStepFunktionaere() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
Text("Schritt 3: Team & Funktionäre", style = MaterialTheme.typography.headlineSmall)
Text("Zuweisung von Richtern und Parcoursbauern (aus ZNS)")
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Turnierbeauftragter (Suche nach Name oder ID)") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Richter (Suche nach Name oder ID)") },
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
fun TournamentStepBewerbe() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxSize()) {
Text("Schritt 4: Bewerbe anlegen", style = MaterialTheme.typography.headlineSmall)
Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
// Left: List of Bewerbe
Card(modifier = Modifier.weight(1f).fillMaxHeight()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Bewerbe", fontWeight = FontWeight.Bold)
TextButton(onClick = {}) { Text("+ Neu") }
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
Text("1: Pony Stilspringprüfung (60cm)", modifier = Modifier.padding(vertical = 4.dp))
Text("2: Einlaufspringprüfung (60cm)", modifier = Modifier.padding(vertical = 4.dp))
Text("...", modifier = Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
// Right: Detail Tabs for selected Bewerb
Card(modifier = Modifier.weight(2f).fillMaxHeight()) {
Column {
// Tabs
PrimaryTabRow(selectedTabIndex = 0) {
Tab(selected = true, onClick = {}, text = { Text("Bewertung") })
Tab(selected = false, onClick = {}, text = { Text("Geldpreis") })
Tab(selected = false, onClick = {}, text = { Text("Ort/Zeit") })
}
// Tab Content (Bewertung)
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = "2",
onValueChange = {},
label = { Text("Bewerb Nr.") },
modifier = Modifier.width(100.dp)
)
OutlinedTextField(
value = "Einlaufspringprüfung",
onValueChange = {},
label = { Text("Bezeichnung") },
modifier = Modifier.weight(1f)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = "60cm",
onValueChange = {},
label = { Text("Klasse / Höhe") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = "§ 218",
onValueChange = {},
label = { Text("Richtverfahren") },
modifier = Modifier.weight(1f)
)
}
Text("Abteilungen", fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = true, onCheckedChange = {})
Text("1. Abt: lizenzfrei")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = true, onCheckedChange = {})
Text("2. Abt: mit Lizenz")
}
}
}
}
}
}
@@ -259,11 +776,11 @@ private fun AuthStatusScreen(
}) { Text("Abmelden") }
Spacer(Modifier.height(8.dp))
OutlinedButton(onClick = onBackToHome) { Text("Zurück zur Startseite") }
OutlinedButton(onClick = onBackToHome) { Text("Zurück zum Dashboard") }
} else {
Text("Nicht angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = onBackToHome) { Text("Zurück zur Startseite") }
Button(onClick = onBackToHome) { Text("Zurück") }
}
}
}