feat(management-feature): add centralized administration screens and back-navigation support

- Introduced comprehensive management screens for horses, riders, clubs, and officials.
- Integrated reusable `ManagementTableScreen` component for standardized layouts and operations.
- Added back-navigation support in `DesktopNavigationPort` with a stack-based implementation.
- Refined `DesktopMainLayout` with enhanced routing and dynamic placeholders for in-development screens.
- Updated roadmap to reflect completion of Phase 7: "Zentrale Verwaltung".

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-01 17:26:44 +02:00
parent 09debdef86
commit 6fc6c8fc79
17 changed files with 1019 additions and 121 deletions
@@ -37,16 +37,17 @@ fun DesktopApp() {
val authState by authTokenManager.authState.collectAsState()
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
// Vision_03 Update: Wir starten direkt in der Veranstaltungs-Übersicht (Offline-First)
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Veranstaltungen
// Vision_03 Update: Wir starten mit Onboarding
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Onboarding
&& currentScreen !is AppScreen.VeranstaltungVerwaltung
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
&& currentScreen !is AppScreen.VeranstaltungUebersicht && currentScreen !is AppScreen.TurnierDetail
&& currentScreen !is AppScreen.TurnierNeu && currentScreen !is AppScreen.Vereine
&& currentScreen !is AppScreen.VeranstaltungProfil && currentScreen !is AppScreen.TurnierDetail
&& currentScreen !is AppScreen.TurnierNeu
) {
LaunchedEffect(Unit) {
// Standard: Direkt zur Veranstaltungs-Übersicht (Offline-First-Modus)
nav.navigateToScreen(AppScreen.Veranstaltungen)
// Standard: Start im Onboarding
nav.navigateToScreen(AppScreen.Onboarding)
}
}
@@ -54,7 +55,7 @@ fun DesktopApp() {
is AppScreen.Login -> LoginScreen(
viewModel = loginViewModel,
onLoginSuccess = {
val returnTo = screen.returnTo ?: AppScreen.Veranstaltungen
val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
nav.navigateToScreen(returnTo)
},
onBack = { /* Desktop hat keine Landing-Page */ },
@@ -65,9 +66,10 @@ fun DesktopApp() {
DesktopMainLayout(
currentScreen = screen,
onNavigate = { nav.navigateToScreen(it) },
onBack = { nav.navigateBack() },
onLogout = {
authTokenManager.clearToken()
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Veranstaltungen))
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung))
},
)
}
@@ -14,14 +14,35 @@ class DesktopNavigationPort : NavigationPort {
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.Onboarding)
override val currentScreen: StateFlow<AppScreen> = _currentScreen.asStateFlow()
// Backstack zur Speicherung des Verlaufs
private val backStack = mutableListOf<AppScreen>()
override fun navigateTo(route: String) {
val screen = AppScreen.fromRoute(route)
println("[DesktopNav] navigateTo $route -> $screen")
_currentScreen.value = screen
navigateToScreen(screen)
}
override fun navigateToScreen(screen: AppScreen) {
println("[DesktopNav] navigateToScreen -> $screen")
// Aktuellen Screen auf den Stack legen, falls er nicht derselbe ist
val current = _currentScreen.value
if (current != screen) {
backStack.add(current)
// Begrenzung des Backstacks auf z.B. 50 Einträge
if (backStack.size > 50) backStack.removeAt(0)
}
_currentScreen.value = screen
}
override fun navigateBack() {
if (backStack.isNotEmpty()) {
val previousScreen = backStack.removeAt(backStack.size - 1)
println("[DesktopNav] navigateBack -> $previousScreen")
_currentScreen.value = previousScreen
} else {
println("[DesktopNav] navigateBack -> Stack leer, bleibe bei Onboarding")
_currentScreen.value = AppScreen.Onboarding
}
}
}
@@ -5,14 +5,11 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.automirrored.filled.Chat
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -56,12 +53,14 @@ private val TopBarTextColor = Color.White
fun DesktopMainLayout(
currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit,
onBack: () -> Unit,
onLogout: () -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
DesktopTopBar(
currentScreen = currentScreen,
onNavigate = onNavigate,
onBack = onBack,
onLogout = onLogout,
)
Column(modifier = Modifier.fillMaxSize()) {
@@ -69,6 +68,7 @@ fun DesktopMainLayout(
DesktopContentArea(
currentScreen = currentScreen,
onNavigate = onNavigate,
onBack = onBack,
)
}
DesktopFooterBar()
@@ -89,6 +89,7 @@ fun DesktopMainLayout(
private fun DesktopTopBar(
currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit,
onBack: () -> Unit,
onLogout: () -> Unit,
) {
Row(
@@ -102,25 +103,25 @@ private fun DesktopTopBar(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Zurück-Pfeil (nur wenn nicht Root)
if (currentScreen !is AppScreen.Veranstaltungen) {
if (currentScreen !is AppScreen.VeranstaltungVerwaltung) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück",
tint = TopBarTextColor,
modifier = Modifier
.size(20.dp)
.clickable { onNavigate(AppScreen.Veranstaltungen) },
.clickable { onBack() },
)
Spacer(Modifier.width(8.dp))
}
// Root-Link
Text(
text = "Veranstaltungen",
text = "Verwaltung",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.clickable { onNavigate(AppScreen.Veranstaltungen) },
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
)
// Breadcrumb-Segmente je nach Screen
@@ -166,7 +167,7 @@ private fun DesktopTopBar(
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.VeranstaltungUebersicht -> {
is AppScreen.VeranstaltungProfil -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter auswählen",
@@ -305,6 +306,27 @@ private fun InvalidContextNotice(message: String, onBack: () -> Unit) {
}
}
@Composable
fun PlaceholderScreen(
title: String,
onBack: () -> Unit,
onAction: (() -> Unit)? = null,
actionLabel: String = "Aktion ausführen"
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(title, style = MaterialTheme.typography.headlineMedium)
Text("Dieser Screen ist noch in Arbeit (Placeholder)", color = Color.Gray)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = onBack) { Text("Zurück") }
if (onAction != null) {
Button(onClick = onAction) { Text(actionLabel) }
}
}
}
}
}
/**
* Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen.
*/
@@ -312,49 +334,133 @@ private fun InvalidContextNotice(message: String, onBack: () -> Unit) {
private fun DesktopContentArea(
currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit,
onBack: () -> Unit,
) {
when (currentScreen) {
// Onboarding ohne Login
is AppScreen.Onboarding -> {
val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject()
// V2 Onboarding (Vision_03)
at.mocode.desktop.v2.OnboardingScreenV2 { _, _ ->
at.mocode.desktop.v2.OnboardingScreen { _, _ ->
authTokenManager.setToken("dummy.jwt.token")
onNavigate(AppScreen.VeranstalterAuswahl)
onNavigate(AppScreen.VeranstaltungVerwaltung)
}
}
// Root-Screen: Leitet in V2-Fluss ab
is AppScreen.Veranstaltungen -> {
at.mocode.desktop.v2.VeranstaltungenUebersichtV2(
onEventOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, eId)) },
onNewEvent = { onNavigate(AppScreen.VeranstaltungKonfig()) }
// Haupt-Zentrale: Veranstaltung-Verwaltung
is AppScreen.VeranstaltungVerwaltung -> {
at.mocode.desktop.v2.VeranstaltungVerwaltungV2(
onVeranstaltungOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) },
onNewVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig()) },
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) },
onNavigateToFunktionaere = { onNavigate(AppScreen.FunktionaerVerwaltung) },
onNavigateToVeranstalter = { onNavigate(AppScreen.VeranstalterVerwaltung) },
onNavigateToZnsImport = { onNavigate(AppScreen.StammdatenImport) }
)
}
// --- ZNS Importer ---
is AppScreen.StammdatenImport -> {
at.mocode.zns.feature.presentation.StammdatenImportScreen(
onBack = onBack
)
}
// --- Pferde-Verwaltung & Profil ---
is AppScreen.PferdVerwaltung -> at.mocode.desktop.v2.PferdeVerwaltungScreen(
onBack = onBack,
onEdit = { onNavigate(AppScreen.PferdProfil(it)) }
)
is AppScreen.PferdProfil -> PlaceholderScreen(
"Pferde-Profil #${currentScreen.id}",
onBack = onBack,
onAction = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
actionLabel = "Zurück zur Zentrale"
)
// --- Reiter-Verwaltung & Profil ---
is AppScreen.ReiterVerwaltung -> at.mocode.desktop.v2.ReiterVerwaltungScreen(
onBack = onBack,
onEdit = { onNavigate(AppScreen.ReiterProfil(it)) }
)
is AppScreen.ReiterProfil -> PlaceholderScreen(
"Reiter-Profil #${currentScreen.id}",
onBack = onBack,
onAction = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
actionLabel = "Zurück zur Zentrale"
)
// --- Verein-Verwaltung & Profil ---
is AppScreen.VereinVerwaltung -> at.mocode.desktop.v2.VereinVerwaltungScreen(
onBack = onBack,
onEdit = { onNavigate(AppScreen.VereinProfil(it)) }
)
is AppScreen.VereinProfil -> PlaceholderScreen(
"Verein-Profil #${currentScreen.id}",
onBack = onBack,
onAction = { onNavigate(AppScreen.VereinVerwaltung) },
actionLabel = "Zurück zur Zentrale"
)
// --- Funktionaer-Verwaltung & Profil ---
is AppScreen.FunktionaerVerwaltung -> at.mocode.desktop.v2.FunktionaerVerwaltungScreen(
onBack = onBack,
onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) }
)
is AppScreen.FunktionaerProfil -> PlaceholderScreen(
"Funktionär-Profil #${currentScreen.id}",
onBack = onBack,
onAction = { onNavigate(AppScreen.FunktionaerVerwaltung) },
actionLabel = "Zurück zur Zentrale"
)
// --- Veranstalter-Verwaltung & Profil ---
is AppScreen.VeranstalterVerwaltung -> at.mocode.desktop.v2.VeranstalterVerwaltungScreen(
onBack = onBack,
onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) }
)
is AppScreen.VeranstalterProfil -> PlaceholderScreen(
"Veranstalter-Profil #${currentScreen.id}",
onBack = onBack,
onAction = { onNavigate(AppScreen.PferdProfil(1L)) },
actionLabel = "Pferde-Profil öffnen"
)
/*
is AppScreen.VeranstaltungProfil -> PlaceholderScreen("Veranstaltung-Profil #${currentScreen.id}",
onBack = { onNavigate(AppScreen.VeranstaltungVerwaltung) }
)
*/
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2(
onBack = { onNavigate(AppScreen.Veranstaltungen) },
onBack = onBack,
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
)
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
onAbbrechen = { onNavigate(AppScreen.VeranstalterAuswahl) },
onSpeichern = { _, _, _ -> onNavigate(AppScreen.VeranstalterAuswahl) },
onAbbrechen = onBack,
onSpeichern = { _, _, _ -> onBack() },
)
is AppScreen.VeranstalterDetail -> {
val vId = currentScreen.veranstalterId
if (!FakeVeranstalterStore.exists(vId)) {
InvalidContextNotice(
message = "Veranstalter (ID=$vId) nicht gefunden.",
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
onBack = onBack
)
} else {
at.mocode.desktop.v2.VeranstalterDetailV2(
veranstalterId = vId,
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) },
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) },
onBack = onBack,
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
)
}
@@ -364,32 +470,30 @@ private fun DesktopContentArea(
// Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard
at.mocode.desktop.v2.VeranstaltungKonfigV2(
veranstalterId = vId,
onBack = {
if (vId == 0L) onNavigate(AppScreen.Veranstaltungen)
else onNavigate(AppScreen.VeranstalterDetail(vId))
},
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) },
onBack = onBack,
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) },
onVeranstalterCreated = { newVId -> onNavigate(AppScreen.VeranstalterDetail(newVId)) }
)
}
is AppScreen.VeranstaltungUebersicht -> {
is AppScreen.VeranstaltungProfil -> {
val vId = currentScreen.veranstalterId
val evtId = currentScreen.veranstaltungId
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) {
InvalidContextNotice(
message = "Veranstalter (ID=$vId) nicht gefunden.",
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
onBack = onBack
)
} else if (at.mocode.desktop.v2.StoreV2.eventsFor(vId).none { it.id == evtId }) {
InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }
onBack = onBack
)
} else {
at.mocode.desktop.v2.VeranstaltungUebersichtV2(
at.mocode.desktop.v2.VeranstaltungProfilScreen(
veranstalterId = vId,
veranstaltungId = evtId,
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onBack = onBack,
onTurnierNeu = {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(vId).firstOrNull { it.id == evtId }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(evtId)
@@ -405,6 +509,7 @@ private fun DesktopContentArea(
onNavigate(AppScreen.TurnierDetail(evtId, newId))
},
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
onNavigateToVeranstalterProfil = { verId -> onNavigate(AppScreen.VeranstalterProfil(verId)) }
)
}
}
@@ -412,7 +517,7 @@ private fun DesktopContentArea(
// Veranstaltungs-Screens
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id,
onBack = { onNavigate(AppScreen.Veranstaltungen) },
onBack = onBack,
onTurnierNeu = {
val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv ->
at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id }
@@ -433,8 +538,8 @@ private fun DesktopContentArea(
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
)
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
onBack = { onNavigate(AppScreen.Veranstaltungen) },
onSave = { onNavigate(AppScreen.Veranstaltungen) },
onBack = onBack,
onSave = { onBack() },
)
// Turnier-Screens
@@ -446,7 +551,7 @@ private fun DesktopContentArea(
if (parent == null) {
InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
onBack = { onNavigate(AppScreen.Veranstaltungen) }
onBack = onBack
)
} else {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(parent.id).firstOrNull { it.id == evtId }
@@ -455,7 +560,7 @@ private fun DesktopContentArea(
TurnierDetailScreen(
veranstaltungId = evtId,
turnierId = currentScreen.turnierId,
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
onBack = onBack,
eventVon = veranstaltung?.datumVon,
eventBis = veranstaltung?.datumBis,
eventOrt = veranstaltung?.ort,
@@ -475,14 +580,14 @@ private fun DesktopContentArea(
if (parent == null) {
InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
onBack = { onNavigate(AppScreen.Veranstaltungen) }
onBack = onBack
)
} else {
at.mocode.desktop.v2.TurnierWizardV2(
veranstalterId = parent.id,
veranstaltungId = evtId,
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
onSaved = { _ -> onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
onBack = onBack,
onSaved = { _ -> onBack() },
)
}
}
@@ -492,7 +597,7 @@ private fun DesktopContentArea(
val pingViewModel: PingViewModel = koinInject()
PingScreen(
viewModel = pingViewModel,
onBack = { onNavigate(AppScreen.Veranstaltungen) },
onBack = onBack,
)
}
@@ -556,7 +661,7 @@ private fun DesktopFooterBar() {
Row(verticalAlignment = Alignment.CenterVertically) {
if (deviceConnected.value) {
OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) {
Icon(Icons.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
Spacer(Modifier.width(6.dp))
Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp)
}
@@ -0,0 +1,276 @@
package at.mocode.desktop.v2
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
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
@Composable
fun <T> ManagementTableScreen(
title: String,
items: List<T>,
columns: List<TableColumn<T>>,
onBack: () -> Unit,
onNew: () -> Unit,
onEdit: (T) -> Unit,
onDelete: (T) -> Unit,
onSearch: (String) -> Unit = {}
) {
var searchQuery by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück")
}
Text(title, style = MaterialTheme.typography.headlineMedium)
}
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = searchQuery,
onValueChange = {
searchQuery = it
onSearch(it)
},
placeholder = { Text("Suchen...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier.width(300.dp).padding(end = 16.dp),
singleLine = true
)
Button(onClick = onNew) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Neu anlegen")
}
}
}
Spacer(Modifier.height(16.dp))
// Tabelle
Card(modifier = Modifier.fillMaxWidth().weight(1f)) {
Column(modifier = Modifier.fillMaxSize()) {
// Table Header
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceVariant,
tonalElevation = 2.dp
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
columns.forEach { col ->
Text(
text = col.header,
modifier = if (col.weight != null) Modifier.weight(col.weight) else Modifier.width(col.width ?: 150.dp),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold
)
}
Spacer(Modifier.width(100.dp)) // Platz für Aktionen
}
}
Divider()
// Table Body
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onEdit(item) }
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
columns.forEach { col ->
Text(
text = col.cellValue(item),
modifier = if (col.weight != null) Modifier.weight(col.weight) else Modifier.width(
col.width ?: 150.dp
),
style = MaterialTheme.typography.bodyMedium
)
}
// Aktionen
Row(modifier = Modifier.width(100.dp), horizontalArrangement = Arrangement.End) {
IconButton(onClick = { onEdit(item) }) {
Icon(Icons.Default.Edit, contentDescription = "Bearbeiten", tint = MaterialTheme.colorScheme.primary)
}
if (title != "Veranstalter-Verwaltung") {
IconButton(onClick = { onDelete(item) }) {
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
}
}
}
}
Divider()
}
}
}
}
}
}
data class TableColumn<T>(
val header: String,
val cellValue: (T) -> String,
val width: androidx.compose.ui.unit.Dp? = null,
val weight: Float? = null
)
@Composable
fun PferdeVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val pferde = StoreV2.pferde
var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) pferde else pferde.filter {
it.name.contains(filter, ignoreCase = true) || it.feiId?.contains(filter, ignoreCase = true) == true
}
ManagementTableScreen(
title = "Pferde-Verwaltung",
items = filteredItems,
columns = listOf(
TableColumn("Name", { it.name }, weight = 1.5f),
TableColumn("ÖPS-Nr.", { it.oepsNummer ?: "-" }, width = 100.dp),
TableColumn("FEI-ID", { it.feiId ?: "-" }, width = 100.dp),
TableColumn("Lebensnr.", { it.lebensnummer ?: "-" }, width = 150.dp),
TableColumn("Geschl.", { it.geschlecht }, width = 80.dp),
TableColumn("Farbe", { it.farbe ?: "-" }, width = 100.dp),
TableColumn("Geb.Datum", { it.geburtsdatum ?: "-" }, width = 100.dp),
TableColumn("Besitzer", { it.besitzer ?: "-" }, weight = 1f)
),
onBack = onBack,
onNew = { /* CRUD Logik */ },
onEdit = { onEdit(it.id) },
onDelete = { StoreV2.pferde.remove(it) },
onSearch = { filter = it }
)
}
@Composable
fun ReiterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val reiter = StoreV2.reiter
var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) reiter else reiter.filter {
it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains(
filter,
ignoreCase = true
) || it.oepsNummer?.contains(filter, ignoreCase = true) == true
}
ManagementTableScreen(
title = "Reiter-Verwaltung",
items = filteredItems,
columns = listOf(
TableColumn("Name", { "${it.vorname} ${it.nachname}" }, weight = 1.5f),
TableColumn("ÖPS-Nr.", { it.oepsNummer ?: "-" }, width = 100.dp),
TableColumn("Lizenz", { it.lizenzKlasse }, width = 100.dp),
TableColumn("Startk.", { if (it.startkartAktiv) "Ja (${it.startkartSaison})" else "Nein" }, width = 100.dp),
TableColumn("Verein", { it.verein ?: "-" }, weight = 1.5f),
TableColumn("Nation", { it.nation }, width = 80.dp)
),
onBack = onBack,
onNew = { },
onEdit = { onEdit(it.id) },
onDelete = { StoreV2.reiter.remove(it) },
onSearch = { filter = it }
)
}
@Composable
fun VereinVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val vereine = StoreV2.vereine
var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) vereine else vereine.filter {
it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true)
}
ManagementTableScreen(
title = "Verein-Verwaltung",
items = filteredItems,
columns = listOf(
TableColumn("Name", { it.name }, weight = 2f),
TableColumn("ÖPS-Nr.", { it.oepsNummer }, width = 100.dp),
TableColumn("BL", { it.bundesland ?: "-" }, width = 60.dp),
TableColumn("Ort", { it.ort ?: "-" }, weight = 1f),
TableColumn("Veranst.", { if (it.istVeranstalter) "Ja" else "Nein" }, width = 80.dp)
),
onBack = onBack,
onNew = { },
onEdit = { onEdit(it.id) },
onDelete = { StoreV2.vereine.remove(it) },
onSearch = { filter = it }
)
}
@Composable
fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val funktionaere = StoreV2.funktionaere
var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) funktionaere else funktionaere.filter {
it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains(filter, ignoreCase = true)
}
ManagementTableScreen(
title = "Funktionär-Verwaltung",
items = filteredItems,
columns = listOf(
TableColumn("Name", { "${it.vorname} ${it.nachname}" }, weight = 1.5f),
TableColumn("Nr.", { it.richterNummer ?: "-" }, width = 100.dp),
TableColumn("Rollen", { it.rollen.joinToString(", ") }, weight = 1.2f),
TableColumn("Quali", { it.richterQualifikation ?: "-" }, width = 120.dp),
TableColumn("Sparten", { it.qualifiziertFuerSparten.joinToString(", ") }, weight = 1.2f)
),
onBack = onBack,
onNew = { },
onEdit = { onEdit(it.id) },
onDelete = { StoreV2.funktionaere.remove(it) },
onSearch = { filter = it }
)
}
@Composable
fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
// Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten
// Wir nutzen hier die 'vereine' Liste aus dem Store.
val vereine = StoreV2.vereine
var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) vereine else vereine.filter {
it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true)
}
ManagementTableScreen(
title = "Veranstalter-Verwaltung",
items = filteredItems,
columns = listOf(
TableColumn("Name", { it.name }, weight = 2f),
TableColumn("ÖPS-Nr.", { it.oepsNummer }, width = 100.dp),
TableColumn("Ort", { it.ort ?: "-" }, weight = 1f),
TableColumn("BL", { it.bundesland ?: "-" }, width = 60.dp),
TableColumn("Email", { it.email ?: "-" }, weight = 1f)
),
onBack = onBack,
onNew = { },
onEdit = { onEdit(it.id) },
onDelete = { },
onSearch = { filter = it }
)
}
@@ -20,7 +20,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@Composable
fun OnboardingScreenV2(onContinue: (String, String) -> Unit) {
fun OnboardingScreen(onContinue: (String, String) -> Unit) {
DesktopThemeV2 {
Surface(color = MaterialTheme.colorScheme.background) {
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -139,9 +139,9 @@ fun VeranstalterDetailV2(
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = verein.ansprechpartner ?: "",
onValueChange = { verein.ansprechpartner = it.ifBlank { null } },
label = { Text("Ansprechpartner (optional)") },
value = verein.ort ?: "",
onValueChange = { verein.ort = it.ifBlank { null } },
label = { Text("Ansprechpartner / Ort (optional)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
@@ -166,9 +166,9 @@ fun VeranstalterDetailV2(
)
}
OutlinedTextField(
value = verein.adresse ?: "",
onValueChange = { verein.adresse = it.ifBlank { null } },
label = { Text("Adresse (optional)") },
value = verein.strasse ?: "",
onValueChange = { verein.strasse = it.ifBlank { null } },
label = { Text("Adresse / Straße (optional)") },
modifier = Modifier.fillMaxWidth(),
minLines = 2
)
@@ -7,13 +7,68 @@ data class Verein(
val id: Long,
var name: String,
var oepsNummer: String,
var ort: String,
// Profil-Felder (minimal laut Abstimmung)
var logoUrl: String? = null,
var ansprechpartner: String? = null,
var kurzname: String? = null,
var bundesland: String? = null,
var ort: String? = null,
var plz: String? = null,
var strasse: String? = null,
var email: String? = null,
var telefon: String? = null,
var adresse: String? = null,
var website: String? = null,
var istVeranstalter: Boolean = false,
var logoUrl: String? = null,
var bemerkungen: String? = null,
)
data class Pferd(
val id: Long,
var name: String,
var geschlecht: String = "Stute",
var geburtsdatum: String? = null,
var rasse: String? = null,
var farbe: String? = null,
var lebensnummer: String? = null,
var chipNummer: String? = null,
var passNummer: String? = null,
var oepsNummer: String? = null,
var feiId: String? = null,
var vater: String? = null,
var mutter: String? = null,
var mutterVater: String? = null,
var stockmass: Int? = null,
var besitzer: String? = null,
var istAktiv: Boolean = true,
)
data class Reiter(
val id: Long,
var vorname: String,
var nachname: String,
var satznummer: String? = null,
var oepsNummer: String? = null,
var feiId: String? = null,
var lizenzKlasse: String = "LIZENZFREI",
var startkartAktiv: Boolean = false,
var startkartSaison: Int? = null,
var geburtsdatum: String? = null,
var vereinsNummer: String? = null,
var verein: String? = null,
var nation: String = "AUT",
var istGastreiter: Boolean = false,
)
data class Funktionaer(
val id: Long,
var vorname: String,
var nachname: String,
var richterNummer: String? = null,
var rollen: List<String> = emptyList(),
var richterQualifikation: String? = null,
var qualifiziertFuerSparten: List<String> = emptyList(),
var email: String? = null,
var telefon: String? = null,
var vereinsNummer: String? = null,
var istAktiv: Boolean = true,
)
data class VeranstaltungV2(
@@ -31,23 +86,185 @@ data class VeranstaltungV2(
)
object StoreV2 {
val pferde: SnapshotStateList<Pferd> = mutableStateListOf(
Pferd(
id = 1,
name = "Don Johnson",
feiId = "104FE22",
geschlecht = "Wallach",
farbe = "Fuchs",
vater = "Don Frederico",
mutter = "Waikiki",
geburtsdatum = "2001-01-01",
besitzer = "Isabell Werth",
lebensnummer = "DE 431316694401",
oepsNummer = "3H66"
),
Pferd(
id = 2,
name = "Bella Rose",
feiId = "103RW04",
geschlecht = "Stute",
farbe = "Fuchs",
vater = "Belissimo M",
mutter = "Cadra II",
geburtsdatum = "2004-01-01",
besitzer = "Madeleine Winter-Schulze",
lebensnummer = "DE 443434443904",
oepsNummer = "2T15"
),
Pferd(
id = 3,
name = "Valegro",
feiId = "102UB51",
geschlecht = "Wallach",
farbe = "Brauner",
vater = "Negro",
mutter = "Maifleur",
geburtsdatum = "2002-01-01",
besitzer = "Carl Hester & Roly Luard",
lebensnummer = "NLD003NL0204840",
oepsNummer = "1V51"
),
Pferd(
id = 4,
name = "Dalera BB",
feiId = "104UD89",
geschlecht = "Stute",
farbe = "Brauner",
vater = "Easy Game",
mutter = "Dark Magic",
geburtsdatum = "2007-01-01",
besitzer = "Beatrice Bürchler-Keller",
lebensnummer = "DE 409090124007",
oepsNummer = "4U89"
),
)
val reiter: SnapshotStateList<Reiter> = mutableStateListOf(
Reiter(
id = 1,
vorname = "Isabell",
nachname = "Werth",
oepsNummer = "O-12345",
feiId = "10011469",
verein = "RFV Graf von Schmettow Eversael",
lizenzKlasse = "RD4",
startkartAktiv = true,
startkartSaison = 2026,
nation = "GER"
),
Reiter(
id = 2,
vorname = "Jessica",
nachname = "von Bredow-Werndl",
oepsNummer = "O-54321",
feiId = "10019075",
verein = "RFV Aubenhausen",
lizenzKlasse = "RD4",
startkartAktiv = true,
startkartSaison = 2026,
nation = "GER"
),
Reiter(
id = 3,
vorname = "Charlotte",
nachname = "Dujardin",
oepsNummer = "GB-9999",
feiId = "10028445",
verein = "Rowallan Activity Centre",
lizenzKlasse = "RD4",
startkartAktiv = true,
startkartSaison = 2026,
nation = "GBR"
),
Reiter(
id = 4,
vorname = "Stefan",
nachname = "Moser",
oepsNummer = "O-44332",
feiId = "10011111",
verein = "URFV Neumarkt/M.",
lizenzKlasse = "R2D2",
startkartAktiv = true,
startkartSaison = 2026,
nation = "AUT",
vereinsNummer = "4-001"
),
)
val funktionaere: SnapshotStateList<Funktionaer> = mutableStateListOf(
Funktionaer(
id = 1,
vorname = "Wolfgang",
nachname = "Schier",
richterNummer = "100123",
rollen = listOf("RICHTER"),
richterQualifikation = "G3",
qualifiziertFuerSparten = listOf("DRESSUR", "SPRINGEN"),
email = "wolfgang.schier@oeps.at",
vereinsNummer = "4-001"
),
Funktionaer(
id = 2,
vorname = "Alice",
nachname = "Schwab",
richterNummer = "100456",
rollen = listOf("RICHTER", "TBA"),
richterQualifikation = "INTERNATIONAL",
qualifiziertFuerSparten = listOf("DRESSUR"),
email = "alice.schwab@oeps.at",
vereinsNummer = "4-002"
),
Funktionaer(
id = 3,
vorname = "Dietmar",
nachname = "Gstöttner",
richterNummer = "100789",
rollen = listOf("PARCOURSBAUER"),
email = "dietmar.gstoettner@oeps.at",
vereinsNummer = "4-003"
),
)
val oepsStammdaten: List<Verein> = listOf(
Verein(1001, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."),
Verein(1002, "Pferdesportverein Linz", "V-OOE-0002", "Linz"),
Verein(1003, "Reitclub Ebelsberg", "V-OOE-0003", "Linz-Ebelsberg"),
Verein(1004, "Union Reitverein Gschwandt", "V-OOE-0004", "Gschwandt"),
Verein(1005, "Reitsportclub Gleisdorf", "V-ST-0005", "Gleisdorf"),
Verein(1006, "Pferdesportzentrum Stadl-Paura", "V-OOE-0006", "Stadl-Paura"),
Verein(
1001,
"Union Reit- und Fahrverein Neumarkt/M.",
"4-001",
ort = "Neumarkt/M.",
bundesland = "",
istVeranstalter = true
),
Verein(1002, "Pferdesportverein Linz", "4-002", ort = "Linz", bundesland = "", istVeranstalter = true),
Verein(1003, "Reitclub Ebelsberg", "4-003", ort = "Linz-Ebelsberg", bundesland = "", istVeranstalter = true),
Verein(1004, "Union Reitverein Gschwandt", "4-004", ort = "Gschwandt", bundesland = "", istVeranstalter = true),
Verein(1005, "Reitsportclub Gleisdorf", "5-005", ort = "Gleisdorf", bundesland = "ST", istVeranstalter = true),
Verein(
1006,
"Pferdesportzentrum Stadl-Paura",
"4-006",
ort = "Stadl-Paura",
bundesland = "",
istVeranstalter = true
),
)
val vereine: SnapshotStateList<Verein> = mutableStateListOf(
Verein(1, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."),
Verein(2, "Pferdesportverein Linz", "V-OOE-0002", "Linz"),
Verein(
1,
"Union Reit- und Fahrverein Neumarkt/M.",
"4-001",
ort = "Neumarkt/M.",
bundesland = "",
istVeranstalter = true
),
Verein(2, "Pferdesportverein Linz", "4-002", ort = "Linz", bundesland = "", istVeranstalter = true),
)
fun addVerein(name: String, oeps: String, ort: String): Long {
val id = (vereine.maxOfOrNull { it.id } ?: 0) + 1
vereine.add(Verein(id, name, oeps, ort))
vereine.add(Verein(id, name, oeps, ort = ort))
return id
}
@@ -96,7 +313,7 @@ object StoreV2 {
datumVon = "2026-05-20",
datumBis = "2026-05-24",
status = "In Vorbereitung",
beschreibung = "Großes Reit-Event am Ebelsberger Schlosspark."
beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark."
)
)
TurnierStoreV2.add(
@@ -28,37 +28,81 @@ import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VeranstaltungenUebersichtV2(
onEventOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId
onNewEvent: () -> Unit
fun VeranstaltungVerwaltungV2(
onVeranstaltungOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId
onNewVeranstaltung: () -> Unit,
onNavigateToPferde: () -> Unit,
onNavigateToReiter: () -> Unit,
onNavigateToVereine: () -> Unit,
onNavigateToFunktionaere: () -> Unit,
onNavigateToVeranstalter: () -> Unit,
onNavigateToZnsImport: () -> Unit
) {
DesktopThemeV2 {
val allEvents = remember { StoreV2.allEvents() }
val allVeranstaltungen = remember { StoreV2.allEvents() }
val vereine = StoreV2.vereine
var searchQuery by remember { mutableStateOf("") }
var selectedStatus by remember { mutableStateOf<String?>(null) }
val availableStatuses = remember(allEvents) { allEvents.map { it.status }.distinct().sorted() }
val availableStatuses = remember(allVeranstaltungen) { allVeranstaltungen.map { it.status }.distinct().sorted() }
val filteredEvents = remember(allEvents, searchQuery, selectedStatus) {
allEvents.filter { event ->
val verein = vereine.find { it.id == event.veranstalterId }
val matchesSearch = event.titel.contains(searchQuery, ignoreCase = true) ||
val filteredVeranstaltungen = remember(allVeranstaltungen, searchQuery, selectedStatus) {
allVeranstaltungen.filter { veranstaltung ->
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
val matchesSearch = veranstaltung.titel.contains(searchQuery, ignoreCase = true) ||
(verein?.name?.contains(searchQuery, ignoreCase = true) ?: false)
val matchesStatus = selectedStatus == null || event.status == selectedStatus
val matchesStatus = selectedStatus == null || veranstaltung.status == selectedStatus
matchesSearch && matchesStatus
}.sortedByDescending { it.datumVon }
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
// Navigation Toolbar (Top)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AssistChip(
onClick = onNavigateToPferde,
label = { Text("Pferde") },
leadingIcon = { Icon(Icons.Default.Pets, null) })
AssistChip(
onClick = onNavigateToReiter,
label = { Text("Reiter") },
leadingIcon = { Icon(Icons.Default.Person, null) })
AssistChip(
onClick = onNavigateToVereine,
label = { Text("Vereine") },
leadingIcon = { Icon(Icons.Default.Home, null) })
AssistChip(
onClick = onNavigateToFunktionaere,
label = { Text("Funktionäre") },
leadingIcon = { Icon(Icons.Default.Badge, null) })
AssistChip(
onClick = onNavigateToVeranstalter,
label = { Text("Veranstalter") },
leadingIcon = { Icon(Icons.Default.Business, null) })
VerticalDivider(Modifier.height(32.dp).padding(horizontal = 4.dp))
AssistChip(
onClick = onNavigateToZnsImport,
label = { Text("ZNS Importer") },
leadingIcon = { Icon(Icons.Default.CloudDownload, null) },
colors = AssistChipDefaults.assistChipColors(
labelColor = MaterialTheme.colorScheme.primary,
leadingIconContentColor = MaterialTheme.colorScheme.primary
)
)
}
// Header
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Alle Veranstaltungen", style = MaterialTheme.typography.headlineMedium)
Button(onClick = onNewEvent) {
Text("Veranstaltung-Verwaltung", style = MaterialTheme.typography.headlineMedium)
Button(onClick = onNewVeranstaltung) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Neue Veranstaltung")
@@ -109,7 +153,7 @@ fun VeranstaltungenUebersichtV2(
}
}
if (filteredEvents.isEmpty()) {
if (filteredVeranstaltungen.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
if (searchQuery.isEmpty() && selectedStatus == null) "Keine Veranstaltungen gefunden."
@@ -119,23 +163,24 @@ fun VeranstaltungenUebersichtV2(
}
} else {
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(filteredEvents) { event ->
val verein = vereine.find { it.id == event.veranstalterId }
items(filteredVeranstaltungen) { veranstaltung ->
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
Card(
modifier = Modifier.fillMaxWidth().clickable { onEventOpen(event.veranstalterId, event.id) },
modifier = Modifier.fillMaxWidth()
.clickable { onVeranstaltungOpen(veranstaltung.veranstalterId, veranstaltung.id) },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text(event.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(veranstaltung.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(
"${verein?.name ?: "Unbekannter Verein"} | ${event.datumVon} bis ${event.datumBis ?: ""}",
"${verein?.name ?: "Unbekannter Verein"} | ${veranstaltung.datumVon} bis ${veranstaltung.datumBis ?: ""}",
style = MaterialTheme.typography.bodySmall
)
if (event.beschreibung.isNotEmpty()) {
if (veranstaltung.beschreibung.isNotEmpty()) {
Spacer(Modifier.height(4.dp))
Text(
event.beschreibung,
veranstaltung.beschreibung,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
color = Color.DarkGray
@@ -147,7 +192,7 @@ fun VeranstaltungenUebersichtV2(
shape = MaterialTheme.shapes.small
) {
Text(
event.status,
veranstaltung.status,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
@@ -213,7 +258,7 @@ fun VeranstalterAnlegenWizard(
if (searchQuery.length < 2) emptyList()
else StoreV2.oepsStammdaten.filter {
it.name.contains(searchQuery, ignoreCase = true) ||
it.ort.contains(searchQuery, ignoreCase = true) ||
(it.ort?.contains(searchQuery, ignoreCase = true) ?: false) ||
it.oepsNummer.contains(searchQuery, ignoreCase = true)
}
}
@@ -223,11 +268,11 @@ fun VeranstalterAnlegenWizard(
items(results) { v ->
ListItem(
headlineContent = { Text(v.name) },
supportingContent = { Text("${v.ort} | ${v.oepsNummer}") },
supportingContent = { Text("${v.ort ?: ""} | ${v.oepsNummer}") },
modifier = Modifier.clickable {
name = v.name
oeps = v.oepsNummer
ort = v.ort
ort = v.ort ?: ""
step = 2
}
)
@@ -409,7 +454,7 @@ fun VeranstaltungKonfigV2(
var search by remember { mutableStateOf("") }
val filteredVereine = remember(search) {
StoreV2.vereine.filter {
it.name.contains(search, ignoreCase = true) || it.ort.contains(search, ignoreCase = true)
it.name.contains(search, ignoreCase = true) || (it.ort?.contains(search, ignoreCase = true) ?: false)
}
}
@@ -444,7 +489,7 @@ fun VeranstaltungKonfigV2(
) {
Column(Modifier.weight(1f)) {
Text(verein.name, fontWeight = FontWeight.Bold)
Text("${verein.ort} | ${verein.oepsNummer}", style = MaterialTheme.typography.bodySmall)
Text("${verein.ort ?: ""} | ${verein.oepsNummer}", style = MaterialTheme.typography.bodySmall)
}
if (isSelected) Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null)
}
@@ -700,12 +745,13 @@ object TurnierStoreV2 {
}
@Composable
fun VeranstaltungUebersichtV2(
fun VeranstaltungProfilScreen(
veranstalterId: Long,
veranstaltungId: Long,
onBack: () -> Unit,
onTurnierNeu: () -> Unit,
onTurnierOpen: (Long) -> Unit,
onNavigateToVeranstalterProfil: (Long) -> Unit,
) {
DesktopThemeV2 {
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
@@ -732,6 +778,15 @@ fun VeranstaltungUebersichtV2(
}
}
Spacer(Modifier.weight(1f))
AssistChip(
onClick = { onNavigateToVeranstalterProfil(veranstalterId) },
label = { Text("Veranstalter-Profil") },
leadingIcon = { Icon(Icons.Default.Business, contentDescription = null, modifier = Modifier.size(18.dp)) }
)
Spacer(Modifier.width(8.dp))
ElevatedButton(
onClick = onTurnierNeu,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
@@ -1244,7 +1299,7 @@ private fun Step2Sparten(
isError = !isDateValid && tVon != null && vVon != null && tVon.isBefore(vVon),
supportingText = {
if (!isDateValid && tVon != null && vVon != null && tVon.isBefore(vVon)) {
Text("Muss innerhalb der Veranstaltung liegen (${veranstaltung?.datumVon})")
Text("Muss innerhalb der Veranstaltung liegen (${veranstaltung.datumVon})")
}
}
)
@@ -1264,7 +1319,7 @@ private fun Step2Sparten(
))),
supportingText = {
if (!isDateValid && tBis != null) {
if (vBis != null && tBis.isAfter(vBis)) Text("Darf nicht nach der Veranstaltung enden (${veranstaltung?.datumBis})")
if (vBis != null && tBis.isAfter(vBis)) Text("Darf nicht nach der Veranstaltung enden (${veranstaltung.datumBis})")
else if (tVon != null && tBis.isBefore(tVon)) Text("Darf nicht vor dem Startdatum liegen")
}
}