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 = "Equest‑Events",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Die kompetente Turnier‑Meldestelle.",
style = MaterialTheme.typography.headlineLarge
text = "Die moderne Turniermeldestelle",
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "Equest‑Events 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 (Pilot‑Partner)") }
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 Equest‑Events 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 C‑NEU‑Turniere. Wir wachsen organisch – Seite an Seite mit unseren Pilot‑Partnern.",
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 = "Regelwerks‑Intelligenz (FEI & ÖTO)",
body = "Wir verbinden ÖTO und FEI – und nehmen Ihnen die Validierungs‑Komplexitä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 & Offline‑fähig",
body = "Stabil auf Laptop und mobil. Dank Offline‑Unterstü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(
}
}
@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)
}
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)
) {
Text(
text = "Willkommen zur Meldestelle",
style = MaterialTheme.typography.headlineMedium
// Data class for dummy tournament
private data class TournamentData(
val id: String,
val date: String,
val title: String,
val location: String
)
// 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.")
@Composable
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")
}
}
}
}
}
// 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,
@Composable
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 = 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Ö"
)
)
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")
}
}
}
}
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") }
}
}
}