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:
+10
-8
@@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+22
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+149
-44
@@ -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)
|
||||
}
|
||||
|
||||
+276
@@ -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 }
|
||||
)
|
||||
}
|
||||
+7
-7
@@ -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
|
||||
)
|
||||
|
||||
+232
-15
@@ -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 = "OÖ",
|
||||
istVeranstalter = true
|
||||
),
|
||||
Verein(1002, "Pferdesportverein Linz", "4-002", ort = "Linz", bundesland = "OÖ", istVeranstalter = true),
|
||||
Verein(1003, "Reitclub Ebelsberg", "4-003", ort = "Linz-Ebelsberg", bundesland = "OÖ", istVeranstalter = true),
|
||||
Verein(1004, "Union Reitverein Gschwandt", "4-004", ort = "Gschwandt", bundesland = "OÖ", 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 = "OÖ",
|
||||
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 = "OÖ",
|
||||
istVeranstalter = true
|
||||
),
|
||||
Verein(2, "Pferdesportverein Linz", "4-002", ort = "Linz", bundesland = "OÖ", 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(
|
||||
|
||||
+84
-29
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user