chore: refaktoriere Veranstaltungs-UI zu Events, implementiere ZNS-Suche und verbessere Navigationslogik

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-21 13:41:06 +02:00
parent 9b4af2bb56
commit 574f8c470c
18 changed files with 673 additions and 174 deletions
@@ -19,7 +19,7 @@ sealed class AppScreen(val route: String) {
data object EntryManagement : AppScreen("/nennung")
// --- Desktop-Navigation (Vision_03) ---
data object VeranstaltungVerwaltung : AppScreen("/verwaltung") // Gesamtübersicht
data object EventVerwaltung : AppScreen("/event/verwaltung") // Gesamtübersicht
// Profile
data object PferdVerwaltung : AppScreen("/pferde/verwaltung")
@@ -45,20 +45,20 @@ sealed class AppScreen(val route: String) {
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
// Neue Veranstaltungs-Konfig-Seite (aus Veranstalter-Detail oder direkt aus Cockpit)
data class VeranstaltungKonfig(val veranstalterId: Long = 0) :
AppScreen("/veranstalter/$veranstalterId/veranstaltung/neu")
data class EventKonfig(val veranstalterId: Long = 0) :
AppScreen("/veranstalter/$veranstalterId/event/neu")
data class VeranstaltungProfil(val veranstalterId: Long, val veranstaltungId: Long) :
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
data class EventProfil(val veranstalterId: Long, val veranstaltungId: Long) :
AppScreen("/veranstalter/$veranstalterId/event/$veranstaltungId")
data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id")
data object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
data class EventDetail(val id: Long) : AppScreen("/event/$id")
data object EventNeu : AppScreen("/event/neu")
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
AppScreen("/veranstaltung/$veranstaltungId/turnier/$turnierId")
AppScreen("/event/$veranstaltungId/turnier/$turnierId")
data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/veranstaltung/$veranstaltungId/turnier/neu")
data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/event/$veranstaltungId/turnier/neu")
data class Billing(val veranstaltungId: Long, val turnierId: Long) :
AppScreen("/veranstaltung/$veranstaltungId/turnier/$turnierId/billing")
AppScreen("/event/$veranstaltungId/turnier/$turnierId/billing")
data object Reiter : AppScreen("/reiter")
data object Pferde : AppScreen("/pferde")
@@ -69,13 +69,13 @@ sealed class AppScreen(val route: String) {
data object NennungsEingang : AppScreen("/nennungs-eingang")
companion object {
private val VERANSTALTUNG_DETAIL = Regex("/veranstaltung/(\\d+)$")
private val TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$")
private val BILLING = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)/billing$")
private val EVENT_DETAIL = Regex("/event/(\\d+)$")
private val TURNIER_DETAIL = Regex("/event/(\\d+)/turnier/(\\d+)$")
private val TURNIER_NEU = Regex("/event/(\\d+)/turnier/neu$")
private val BILLING = Regex("/event/(\\d+)/turnier/(\\d+)/billing$")
private val VERANSTALTER_DETAIL = Regex("/veranstalter/(\\d+)$")
private val VERANSTALTUNG_KONFIG = Regex("/veranstalter/(\\d+)/veranstaltung/neu$")
private val VERANSTALTUNG_PROFIL = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$")
private val EVENT_KONFIG = Regex("/veranstalter/(\\d+)/event/neu$")
private val EVENT_PROFIL = Regex("/veranstalter/(\\d+)/event/(\\d+)$")
private val PFERD_PROFIL = Regex("/pferde/profil/(\\d+)$")
private val REITER_PROFIL = Regex("/reiter/profil/(\\d+)$")
@@ -98,14 +98,14 @@ sealed class AppScreen(val route: String) {
"/organizer/profile" -> OrganizerProfile
"/auth/callback" -> AuthCallback
"/nennung" -> EntryManagement
"/verwaltung" -> VeranstaltungVerwaltung
"/event/verwaltung" -> EventVerwaltung
"/pferde/verwaltung" -> PferdVerwaltung
"/reiter/verwaltung" -> ReiterVerwaltung
"/vereine/verwaltung" -> VereinVerwaltung
"/funktionaere/verwaltung" -> FunktionaerVerwaltung
"/veranstalter/verwaltung" -> VeranstalterVerwaltung
"/veranstalter/auswahl" -> VeranstalterAuswahl
"/veranstaltung/neu" -> VeranstaltungNeu
"/event/neu" -> EventNeu
"/meisterschaften" -> Meisterschaften
"/cups" -> Cups
"/stammdaten/import" -> StammdatenImport
@@ -120,7 +120,7 @@ sealed class AppScreen(val route: String) {
FUNKTIONAER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return FunktionaerProfil(id.toLong()) }
VERANSTALTER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstalterProfil(id.toLong()) }
/*
VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstaltungProfil(id.toLong()) }
EVENT_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return EventProfil(id.toLong()) }
*/
TURNIER_DETAIL.matchEntire(route)?.destructured?.let { (vId, tId) ->
@@ -129,17 +129,17 @@ sealed class AppScreen(val route: String) {
TURNIER_NEU.matchEntire(route)?.destructured?.let { (vId) ->
return TurnierNeu(vId.toLong())
}
VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
return VeranstaltungDetail(id.toLong())
EVENT_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
return EventDetail(id.toLong())
}
VERANSTALTER_DETAIL.matchEntire(route)?.destructured?.let { (vId) ->
return VeranstalterDetail(vId.toLong())
}
VERANSTALTUNG_KONFIG.matchEntire(route)?.destructured?.let { (vId) ->
return VeranstaltungKonfig(vId.toLong())
EVENT_KONFIG.matchEntire(route)?.destructured?.let { (vId) ->
return EventKonfig(vId.toLong())
}
VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
return VeranstaltungProfil(verId.toLong(), vId.toLong())
EVENT_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
return EventProfil(verId.toLong(), vId.toLong())
}
PortalDashboard // Default fallback
}
@@ -1,6 +1,7 @@
package at.mocode.frontend.features.profile.di
import at.mocode.frontend.features.profile.data.ProfileApiClient
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -8,4 +9,5 @@ import org.koin.dsl.module
val profileModule = module {
single { ProfileApiClient(get(named("apiClient")), get()) }
single { ProfileViewModel(get()) }
factory { ProfileOnboardingViewModel(get(), get()) }
}
@@ -0,0 +1,169 @@
package at.mocode.frontend.features.profile.presentation
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.CheckCircle
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.MsTextField
@Composable
fun ProfileOnboardingScreen(
viewModel: ProfileOnboardingViewModel,
onFinish: () -> Unit
) {
val state = viewModel.state
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = "Willkommen bei der Meldestelle",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
LinearProgressIndicator(
progress = {
when (state.currentStep) {
OnboardingStep.SEARCH_ZNS -> 0.33f
OnboardingStep.CONFIRM_DATA -> 0.66f
OnboardingStep.FINISHED -> 1f
}
},
modifier = Modifier.fillMaxWidth()
)
Box(modifier = Modifier.weight(1f)) {
when (state.currentStep) {
OnboardingStep.SEARCH_ZNS -> SearchStep(viewModel)
OnboardingStep.CONFIRM_DATA -> ConfirmStep(viewModel)
OnboardingStep.FINISHED -> FinishedStep(state, onFinish)
}
}
if (state.currentStep != OnboardingStep.FINISHED) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(onClick = { viewModel.back() }, enabled = state.currentStep != OnboardingStep.SEARCH_ZNS) {
Text("Zurück")
}
if (state.currentStep == OnboardingStep.CONFIRM_DATA) {
Button(onClick = { viewModel.confirmAndLink() }, enabled = !state.isLoading) {
if (state.isLoading) CircularProgressIndicator(Modifier.size(16.dp))
else Text("Daten bestätigen & Verknüpfen")
}
}
}
}
}
}
@Composable
private fun SearchStep(viewModel: ProfileOnboardingViewModel) {
val state = viewModel.state
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Wer bist du?", style = MaterialTheme.typography.titleLarge)
Text("Suchen Sie nach Ihrer Satznummer oder Ihrem Namen in den ZNS-Stammdaten.")
MsTextField(
value = state.searchQuery,
onValueChange = { viewModel.onSearchQueryChange(it) },
label = "Suche (Name oder Satznummer)",
placeholder = "z.B. Stroblmair",
modifier = Modifier.fillMaxWidth(),
leadingIcon = Icons.Default.Search
)
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(state.searchResults) { reiter ->
Card(
onClick = { viewModel.selectReiter(reiter) },
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(Icons.Default.Person, null)
Column {
Text("${reiter.vorname} ${reiter.nachname}", fontWeight = FontWeight.Bold)
Text("Satznr: ${reiter.satznummer ?: "N/A"} | Lizenz: ${reiter.lizenz ?: "Keine"}")
}
}
}
}
}
}
}
@Composable
private fun ConfirmStep(viewModel: ProfileOnboardingViewModel) {
val state = viewModel.state
val reiter = state.selectedReiter ?: return
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Daten bestätigen", style = MaterialTheme.typography.titleLarge)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Vorname: ${reiter.vorname}")
Text("Nachname: ${reiter.nachname}")
Text("Satznummer: ${reiter.satznummer ?: "N/A"}")
Text("Lizenz: ${reiter.lizenz ?: "Keine"}")
Text("Klasse: ${reiter.lizenzKlasse}")
}
}
Text(
"Durch das Verknüpfen werden Ihre Aktionen in der App mit Ihrer offiziellen ZNS-Identität hinterlegt.",
style = MaterialTheme.typography.bodyMedium
)
if (state.error != null) {
Text(state.error, color = MaterialTheme.colorScheme.error)
}
}
}
@Composable
private fun FinishedStep(state: ProfileOnboardingState, onFinish: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.height(16.dp))
Text("Profil erfolgreich verknüpft!", style = MaterialTheme.typography.headlineSmall)
Text("Willkommen, ${state.selectedReiter?.vorname ?: ""} ${state.selectedReiter?.nachname ?: ""}!")
Spacer(Modifier.height(32.dp))
Button(onClick = onFinish) {
Text("Los geht's")
}
}
}
@@ -0,0 +1,98 @@
package at.mocode.frontend.features.profile.presentation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
import at.mocode.frontend.features.profile.data.ProfileApiClient
import at.mocode.frontend.features.profile.data.ProfileDto
import kotlinx.coroutines.launch
enum class OnboardingStep {
SEARCH_ZNS,
CONFIRM_DATA,
FINISHED
}
data class ProfileOnboardingState(
val currentStep: OnboardingStep = OnboardingStep.SEARCH_ZNS,
val searchQuery: String = "",
val searchResults: List<ZnsRemoteReiter> = emptyList(),
val selectedReiter: ZnsRemoteReiter? = null,
val isLoading: Boolean = false,
val error: String? = null,
val profile: ProfileDto? = null
)
class ProfileOnboardingViewModel(
private val znsImportProvider: ZnsImportProvider,
private val profileApiClient: ProfileApiClient
) : ViewModel() {
var state by mutableStateOf(ProfileOnboardingState())
private set
fun onSearchQueryChange(query: String) {
state = state.copy(searchQuery = query)
if (query.length >= 3) {
search()
}
}
private fun search() {
viewModelScope.launch {
state = state.copy(isLoading = true, error = null)
try {
znsImportProvider.searchRemote(state.searchQuery)
state = state.copy(
isLoading = false,
searchResults = znsImportProvider.state.remoteReiter
)
} catch (e: Exception) {
state = state.copy(isLoading = false, error = "Fehler bei der ZNS-Suche: ${e.message}")
}
}
}
fun selectReiter(reiter: ZnsRemoteReiter) {
state = state.copy(
selectedReiter = reiter,
currentStep = OnboardingStep.CONFIRM_DATA
)
}
fun confirmAndLink() {
val reiter = state.selectedReiter ?: return
viewModelScope.launch {
state = state.copy(isLoading = true, error = null)
try {
val satznr = reiter.satznummer ?: ""
val profile = profileApiClient.linkToZns(satznr)
if (profile != null) {
state = state.copy(
isLoading = false,
profile = profile,
currentStep = OnboardingStep.FINISHED
)
} else {
state = state.copy(isLoading = false, error = "Verknüpfung fehlgeschlagen.")
}
} catch (e: Exception) {
state = state.copy(isLoading = false, error = "Fehler beim Verknüpfen: ${e.message}")
}
}
}
fun back() {
state = state.copy(
currentStep = when (state.currentStep) {
OnboardingStep.SEARCH_ZNS -> OnboardingStep.SEARCH_ZNS
OnboardingStep.CONFIRM_DATA -> OnboardingStep.SEARCH_ZNS
OnboardingStep.FINISHED -> OnboardingStep.CONFIRM_DATA
}
)
}
}
@@ -231,7 +231,7 @@ private fun StepOrtZeit(state: CreateBewerbWizardState, onStateChange: (CreateBe
@Composable
private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
Column(Modifier.fillMaxWidth()) {
// Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weiterer Prüfung -> TB-Hinweis
// Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weitere Prüfung TB-Hinweis
val warnTb = state.richter.isNotEmpty()
if (warnTb) {
Box(
@@ -240,6 +240,25 @@ private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (C
Spacer(Modifier.height(8.dp))
}
// Abteilungs-Vorschau (§ 39 ÖTO)
val abteilungsInfo = remember(state.klasse, state.teilungsTyp) {
when {
state.klasse.contains("S", ignoreCase = true) -> "§ 39 ÖTO: Abteilungstrennung ab 35 Nennungen (R1 getrennt von R2+)"
state.klasse.contains("M", ignoreCase = true) -> "§ 39 ÖTO: Abteilungstrennung ab 50 Nennungen"
else -> "Standard-Abteilungstrennung gemäß ÖTO § 39"
}
}
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Column(Modifier.padding(12.dp)) {
Text("Abteilungs-Vorschau (§ 39 ÖTO)", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
Text(abteilungsInfo, style = MaterialTheme.typography.bodySmall)
}
}
OutlinedTextField(
value = state.teilungsTyp,
onValueChange = { onStateChange(state.copy(teilungsTyp = it)) },
@@ -1,5 +1,6 @@
package at.mocode.veranstaltung.feature.di
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel
import org.koin.core.qualifier.named
@@ -7,5 +8,5 @@ import org.koin.dsl.module
val veranstaltungModule = module {
factory { VeranstaltungManagementViewModel(get()) }
factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get()) }
factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get(), get<ZnsImportProvider>(), get()) }
}
@@ -49,7 +49,7 @@ fun VeranstaltungDetailScreen(
val event = veranstaltung
if (event == null) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Veranstaltung #$veranstaltungId nicht gefunden.")
Text("Event #$veranstaltungId nicht gefunden.")
}
return
}
@@ -95,7 +95,7 @@ fun VeranstaltungDetailScreen(
}
Text(
text = "Turniere in dieser Veranstaltung",
text = "Turniere in diesem Event",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -18,9 +18,7 @@ import at.mocode.frontend.core.designsystem.components.MsFilePicker
import at.mocode.frontend.core.designsystem.components.MsTextField
import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.features.turnier.presentation.TurnierWizard
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
import org.koin.compose.koinInject
import kotlin.uuid.ExperimentalUuidApi
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
@@ -37,7 +35,7 @@ fun VeranstaltungWizardScreen(
topBar = {
Column {
TopAppBar(
title = { Text("Neue Veranstaltung anlegen") },
title = { Text("Neues Event anlegen") },
navigationIcon = {
IconButton(onClick = {
if (state.currentStep == WizardStep.ZNS_CHECK) onBack()
@@ -108,7 +106,7 @@ private fun VorschauCard(state: VeranstaltungWizardState) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.name.ifBlank { "Neue Veranstaltung" },
text = state.name.ifBlank { "Neues Event" },
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -275,7 +273,31 @@ private fun VeranstalterSelectionStep(
}
}
}
} else {
}
if (viewModel.state.znsSearchResults.isNotEmpty()) {
Text("Gefundene Vereine in den Stammdaten:", style = MaterialTheme.typography.labelMedium)
viewModel.state.znsSearchResults.forEach { znsVerein ->
Card(
modifier = Modifier.fillMaxWidth(),
onClick = { viewModel.selectZnsVerein(znsVerein) }
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(Icons.Default.Add, null)
Column {
Text(znsVerein.name, fontWeight = FontWeight.Medium)
Text("OEPS-Nr: ${znsVerein.oepsNummer} | ${znsVerein.ort ?: ""}", style = MaterialTheme.typography.bodySmall)
}
}
}
}
}
if (viewModel.state.veranstalterId == null && viewModel.state.znsSearchResults.isEmpty()) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -400,13 +422,13 @@ private fun MetaDataStep(viewModel: VeranstaltungWizardViewModel) {
@Composable
private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
val state = viewModel.state
val turnierViewModel = viewModel.turnierWizardViewModel
var showWizard by remember { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge)
if (showWizard) {
val turnierViewModel = koinInject<TurnierWizardViewModel>()
Card(modifier = Modifier.fillMaxWidth().height(500.dp)) {
TurnierWizard(
viewModel = turnierViewModel,
@@ -414,7 +436,7 @@ private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
onBack = { showWizard = false },
onFinish = {
showWizard = false
viewModel.addTurnier() // Dummy zum Hinzufügen im Haupt-Wizard
viewModel.addTurnier(turnierViewModel.state.turnierNr, "")
}
)
}
@@ -9,7 +9,10 @@ import at.mocode.core.domain.serialization.UuidSerializer
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
import at.mocode.frontend.core.domain.repository.MasterdataRepository
import at.mocode.frontend.core.domain.repository.MasterdataStats
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
import at.mocode.frontend.core.network.NetworkConfig
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
import at.mocode.frontend.features.verein.domain.VereinRepository
import io.ktor.client.*
import io.ktor.client.request.*
@@ -55,7 +58,8 @@ data class VeranstaltungWizardState(
val createdVeranstaltungId: Uuid? = null,
val isZnsAvailable: Boolean = false,
val stammdatenStats: MasterdataStats? = null,
val isCheckingStats: Boolean = false
val isCheckingStats: Boolean = false,
val znsSearchResults: List<ZnsRemoteVerein> = emptyList()
)
@OptIn(ExperimentalUuidApi::class)
@@ -63,7 +67,9 @@ class VeranstaltungWizardViewModel(
private val httpClient: HttpClient,
private val authTokenManager: AuthTokenManager,
private val vereinRepository: VereinRepository,
private val masterdataRepository: MasterdataRepository
private val masterdataRepository: MasterdataRepository,
private val znsImportProvider: ZnsImportProvider,
val turnierWizardViewModel: TurnierWizardViewModel // Injected Child-ViewModel
) : ViewModel() {
var state by mutableStateOf(VeranstaltungWizardState())
@@ -98,19 +104,45 @@ class VeranstaltungWizardViewModel(
fun searchVeranstalterByOepsNr(oepsNr: String) {
viewModelScope.launch {
val verein = vereinRepository.findByOepsNr(oepsNr)
if (verein != null) {
setVeranstalter(
id = Uuid.parse(verein.id),
nummer = verein.oepsNr ?: "",
name = verein.name,
standardOrt = "${verein.plz ?: ""} ${verein.ort ?: ""}".trim(),
logo = null // Hier könnte später ein Logo-Service greifen
)
try {
val verein = vereinRepository.findByOepsNr(oepsNr)
if (verein != null) {
// Robustes Parsing für Mock-Daten (z. B. "v1")
val uuid = try {
Uuid.parse(verein.id)
} catch (_: Exception) {
// Fallback für Mock-IDs während der Entwicklung
Uuid.random()
}
setVeranstalter(
id = uuid,
nummer = verein.oepsNr ?: "",
name = verein.name,
standardOrt = "${verein.plz ?: ""} ${verein.ort ?: ""}".trim(),
logo = null
)
} else if (oepsNr.length >= 3) {
// Suche in den ZNS-Stammdaten als Fallback
znsImportProvider.searchRemote(oepsNr)
state = state.copy(znsSearchResults = znsImportProvider.state.remoteResults)
}
} catch (e: Exception) {
state = state.copy(error = "Fehler bei der Veranstalter-Suche: ${e.message}")
}
}
}
fun selectZnsVerein(znsVerein: ZnsRemoteVerein) {
setVeranstalter(
id = Uuid.random(), // Neuer Veranstalter wird angelegt
nummer = znsVerein.oepsNummer,
name = znsVerein.name,
standardOrt = znsVerein.ort ?: "",
logo = null
)
}
fun nextStep() {
state = state.copy(
currentStep = when (state.currentStep) {
@@ -155,23 +187,13 @@ class VeranstaltungWizardViewModel(
state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end, logoUrl = logo)
}
fun updateTurnier(index: Int, nummer: String, path: String?) {
val newList = state.turniere.toMutableList()
if (index in newList.indices) {
newList[index] = newList[index].copy(nummer = nummer, ausschreibungPath = path)
state = state.copy(turniere = newList)
}
}
fun addTurnier() {
state = state.copy(turniere = state.turniere + TurnierEntry())
fun addTurnier(nummer: String = "", pfad: String? = null) {
state = state.copy(turniere = state.turniere + TurnierEntry(nummer = nummer, ausschreibungPath = pfad))
}
fun removeTurnier(index: Int) {
if (state.turniere.size > 1) {
val newList = state.turniere.toMutableList().apply { removeAt(index) }
state = state.copy(turniere = newList)
}
val newList = state.turniere.toMutableList().apply { removeAt(index) }
state = state.copy(turniere = newList)
}
fun saveVeranstaltung() {
@@ -43,12 +43,12 @@ fun VeranstaltungenScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Veranstaltungen - verwalten",
text = "Events - verwalten",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
MsButton(
text = "Neue Veranstaltung",
text = "Neues Event",
onClick = onVeranstaltungNeu
)
}
@@ -119,7 +119,7 @@ fun VeranstaltungenScreen(
)
Spacer(Modifier.height(Dimens.SpacingM))
Text(
"Keine Veranstaltungen gefunden.",
"Keine Events gefunden.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -38,7 +38,10 @@ fun DesktopApp() {
// DeviceInitialization-Check beim Start
LaunchedEffect(Unit) {
if (!DeviceInitializationSettingsManager.isConfigured()) {
println("[DesktopApp] Setup fehlt -> Umleitung zum DeviceInitialization")
nav.navigateToScreen(AppScreen.DeviceInitialization)
} else {
println("[DesktopApp] Setup vorhanden.")
}
}
@@ -47,22 +50,32 @@ fun DesktopApp() {
// Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt
// Vision_03 Update: Wir starten mit DeviceInitialization
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.DeviceInitialization
&& currentScreen !is AppScreen.VeranstaltungVerwaltung
&& currentScreen !is AppScreen.EventVerwaltung
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
&& currentScreen !is AppScreen.VeranstaltungProfil && currentScreen !is AppScreen.TurnierDetail
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.EventKonfig
&& currentScreen !is AppScreen.EventProfil && currentScreen !is AppScreen.TurnierDetail
&& currentScreen !is AppScreen.TurnierNeu
&& currentScreen !is AppScreen.ReiterVerwaltung
&& currentScreen !is AppScreen.PferdVerwaltung
&& currentScreen !is AppScreen.VereinVerwaltung
&& currentScreen !is AppScreen.ReiterVerwaltung && currentScreen !is AppScreen.Reiter
&& currentScreen !is AppScreen.PferdVerwaltung && currentScreen !is AppScreen.Pferde
&& currentScreen !is AppScreen.VereinVerwaltung && currentScreen !is AppScreen.Vereine
&& currentScreen !is AppScreen.FunktionaerVerwaltung && currentScreen !is AppScreen.FunktionaerProfil
&& currentScreen !is AppScreen.ReiterProfil
&& currentScreen !is AppScreen.PferdProfil
&& currentScreen !is AppScreen.VereinProfil
&& currentScreen !is AppScreen.StammdatenImport
&& currentScreen !is AppScreen.NennungsEingang
&& currentScreen !is AppScreen.VeranstaltungNeu
&& currentScreen !is AppScreen.EventNeu
&& currentScreen !is AppScreen.ConnectivityCheck
&& currentScreen !is AppScreen.Dashboard
) {
LaunchedEffect(Unit) {
// Standard: Start im DeviceInitialization
nav.navigateToScreen(AppScreen.DeviceInitialization)
LaunchedEffect(currentScreen) {
if (!DeviceInitializationSettingsManager.isConfigured()) {
println("[DesktopApp] Nicht authentifiziert & nicht konfiguriert -> Setup")
nav.navigateToScreen(AppScreen.DeviceInitialization)
} else {
println("[DesktopApp] Nicht authentifiziert, aber konfiguriert -> Dashboard")
nav.navigateToScreen(AppScreen.EventVerwaltung)
}
}
}
@@ -70,7 +83,7 @@ fun DesktopApp() {
is AppScreen.Login -> LoginScreen(
viewModel = loginViewModel,
onLoginSuccess = {
val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
val returnTo = screen.returnTo ?: AppScreen.EventVerwaltung
nav.navigateToScreen(returnTo)
},
onBack = { nav.navigateBack() },
@@ -84,7 +97,7 @@ fun DesktopApp() {
onBack = { nav.navigateBack() },
onLogout = {
authTokenManager.clearToken()
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung))
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.EventVerwaltung))
},
isAuthenticated = authState.isAuthenticated
)
@@ -45,13 +45,15 @@ fun DesktopMainLayout(
}
// Automatische Umleitung zum DeviceInitialization, wenn Setup fehlt (außer wir sind bereits dort)
LaunchedEffect(onboardingSettings) {
LaunchedEffect(currentScreen) {
if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.DeviceInitialization) {
println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization")
onNavigate(AppScreen.DeviceInitialization)
} else if (onboardingSettings.isConfigured && currentScreen is AppScreen.DeviceInitialization) {
println("[DesktopNav] Setup abgeschlossen -> Wechsel zum Dashboard")
onNavigate(AppScreen.VeranstaltungVerwaltung)
// Falls wir konfiguriert sind, aber im Setup-Screen landen (z.B. durch manuellen Nav-Call),
// erlauben wir den Aufenthalt dort (für Edit), aber forcieren keinen Redirect zum Dashboard hier,
// da dies der Wizard am Ende selbst macht.
println("[DesktopNav] Setup vorhanden und im Setup-Screen.")
}
}
@@ -27,7 +27,8 @@ import at.mocode.frontend.features.pferde.presentation.PferdeScreen
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
import at.mocode.frontend.features.ping.presentation.PingScreen
import at.mocode.frontend.features.ping.presentation.PingViewModel
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingWizard
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingScreen
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
import at.mocode.frontend.features.profile.presentation.ProfileScreen
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
@@ -70,17 +71,18 @@ fun DesktopContentArea(
val authTokenManager = org.koin.core.context.GlobalContext.get().get<AuthTokenManager>()
authTokenManager.setToken(finalSettings.sharedKey)
onSettingsChange(finalSettings)
onNavigate(AppScreen.VeranstaltungVerwaltung)
// nav.navigateToScreen(...) wird hier nicht direkt gerufen, sondern onNavigate
onNavigate(AppScreen.EventVerwaltung)
})
}
DeviceInitializationScreen(viewModel = viewModel)
}
// Haupt-Zentrale: Veranstaltung-Verwaltung
is AppScreen.VeranstaltungVerwaltung -> {
// Haupt-Zentrale: Event-Verwaltung
is AppScreen.EventVerwaltung -> {
VeranstaltungenScreen(
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu) },
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.EventProfil(vId, eId)) }
)
}
@@ -91,6 +93,15 @@ fun DesktopContentArea(
)
}
// --- Profile Onboarding ---
is AppScreen.ProfileOnboarding -> {
val viewModel = koinViewModel<ProfileOnboardingViewModel>()
ProfileOnboardingScreen(
viewModel = viewModel,
onFinish = { onNavigate(AppScreen.EventVerwaltung) }
)
}
// --- Pferde-Verwaltung & Profil ---
is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
val viewModel = koinViewModel<PferdeViewModel>()
@@ -165,14 +176,14 @@ fun DesktopContentArea(
is AppScreen.VeranstalterProfil -> VeranstalterDetail(
veranstalterId = currentScreen.id,
onBack = onBack,
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungNeu) },
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.EventProfil(currentScreen.id, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.EventNeu) },
)
// Neuer Flow: Veranstalter auswählen → Veranstaltung-Wizard
// Neuer Flow: Veranstalter auswählen → Event-Wizard
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
onBack = onBack,
onWeiter = { _ -> onNavigate(AppScreen.VeranstaltungNeu) },
onWeiter = { _ -> onNavigate(AppScreen.EventNeu) },
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
)
@@ -186,12 +197,12 @@ fun DesktopContentArea(
VeranstalterDetail(
veranstalterId = vId,
onBack = onBack,
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.EventProfil(vId, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.EventKonfig(vId)) },
)
}
is AppScreen.VeranstaltungKonfig -> {
is AppScreen.EventKonfig -> {
val vId = currentScreen.veranstalterId
VeranstaltungKonfigScreen(
veranstalterId = vId,
@@ -201,12 +212,12 @@ fun DesktopContentArea(
// val allEvents = Store.allEvents()
// val newId = (allEvents.maxOfOrNull { it.id } ?: 0L) + 1L
// ...
onNavigate(AppScreen.VeranstaltungProfil(vId, 0L)) // Mock
onNavigate(AppScreen.EventProfil(vId, 0L)) // Mock
}
)
}
is AppScreen.VeranstaltungProfil -> {
is AppScreen.EventProfil -> {
VeranstaltungProfilScreen(
veranstalterId = currentScreen.veranstalterId,
veranstaltungId = currentScreen.veranstaltungId,
@@ -223,7 +234,7 @@ fun DesktopContentArea(
)
}
is AppScreen.VeranstaltungDetail -> {
is AppScreen.EventDetail -> {
val repository: at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository = koinInject()
VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id,
@@ -235,7 +246,7 @@ fun DesktopContentArea(
)
}
is AppScreen.VeranstaltungNeu -> {
is AppScreen.EventNeu -> {
val viewModel: at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel = koinViewModel()
at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardScreen(
viewModel = viewModel,
@@ -323,18 +334,13 @@ fun DesktopContentArea(
ProfileScreen(viewModel = viewModel)
}
is AppScreen.ProfileOnboarding -> {
val viewModel = koinViewModel<ProfileViewModel>()
ProfileOnboardingWizard(
viewModel = viewModel,
onFinish = { onNavigate(AppScreen.Dashboard) }
)
}
is AppScreen.Home, is AppScreen.Dashboard -> {
is AppScreen.Home, is AppScreen.Dashboard, is AppScreen.PortalDashboard,
is AppScreen.Meisterschaften, is AppScreen.Cups,
is AppScreen.CreateTournament, is AppScreen.OrganizerProfile -> {
AdminUebersichtScreen(
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) }
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.EventDetail(id)) }
)
}
@@ -35,21 +35,13 @@ fun DesktopNavRail(
icon = Icons.Default.Adjust,
label = "Logo",
selected = false,
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
onClick = { onNavigate(AppScreen.EventVerwaltung) },
enabled = isConfigured
)
Spacer(Modifier.height(Dimens.SpacingL))
// Navigations-Items
NavRailItem(
icon = Icons.Default.Dashboard,
label = "Admin",
selected = currentScreen is AppScreen.VeranstaltungVerwaltung || currentScreen is AppScreen.VeranstaltungDetail,
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
enabled = isConfigured
)
NavRailItem(
icon = Icons.Default.CloudDownload,
label = "ZNS-Import",
@@ -101,7 +93,7 @@ fun DesktopNavRail(
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
)
DropdownMenuItem(
text = { Text("Richter") },
text = { Text("Funktionäre") },
onClick = {
showStammdatenMenu = false
onNavigate(AppScreen.FunktionaerVerwaltung)
@@ -111,6 +103,43 @@ fun DesktopNavRail(
}
}
var showVerwaltungMenu by remember { mutableStateOf(false) }
Box {
NavRailItem(
icon = Icons.Default.Dashboard,
label = "Verwaltungen",
selected = currentScreen is AppScreen.EventVerwaltung ||
currentScreen is AppScreen.EventDetail ||
currentScreen is AppScreen.VeranstalterVerwaltung ||
currentScreen is AppScreen.VeranstalterAuswahl,
onClick = { showVerwaltungMenu = true },
enabled = isConfigured
)
DropdownMenu(
expanded = showVerwaltungMenu && isConfigured,
onDismissRequest = { showVerwaltungMenu = false },
offset = DpOffset(Dimens.NavRailWidth, 0.dp)
) {
DropdownMenuItem(
text = { Text("Veranstalter") },
onClick = {
showVerwaltungMenu = false
onNavigate(AppScreen.VeranstalterVerwaltung)
},
leadingIcon = { Icon(Icons.Default.Business, contentDescription = null) }
)
DropdownMenuItem(
text = { Text("Events") },
onClick = {
showVerwaltungMenu = false
onNavigate(AppScreen.EventVerwaltung)
},
leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }
)
}
}
NavRailItem(
icon = Icons.Default.Email,
label = "Mails",
@@ -43,7 +43,7 @@ fun DesktopTopHeader(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Zurück-Button ausblenden auf Startseite oder im Setup
if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.VeranstaltungVerwaltung) {
if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.EventVerwaltung) {
IconButton(
onClick = {
// Verhindere Rücksprung zum Setup, wenn konfiguriert
@@ -65,7 +65,7 @@ fun DesktopTopHeader(
// Home Icon als Anker
IconButton(
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
onClick = { onNavigate(AppScreen.EventVerwaltung) },
modifier = Modifier.size(Dimens.IconSizeM),
enabled = isConfigured
) {
@@ -207,7 +207,7 @@ private fun BreadcrumbContent(
)
}
is AppScreen.VeranstaltungProfil -> {
is AppScreen.EventProfil -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
@@ -224,43 +224,43 @@ private fun BreadcrumbContent(
)
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
is AppScreen.VeranstaltungVerwaltung -> {
is AppScreen.EventVerwaltung -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltungs-Verwaltung",
text = "Event-Verwaltung",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
is AppScreen.VeranstaltungDetail -> {
is AppScreen.EventDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltungs-Verwaltung",
text = "Event-Verwaltung",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
modifier = Modifier.clickable { onNavigate(AppScreen.EventVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.id}",
text = "Event #${currentScreen.id}",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
is AppScreen.VeranstaltungNeu -> {
is AppScreen.EventNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltungs-Verwaltung",
text = "Event-Verwaltung",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
modifier = Modifier.clickable { onNavigate(AppScreen.EventVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Neue Veranstaltung",
text = "Neues Event",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
@@ -268,10 +268,10 @@ private fun BreadcrumbContent(
is AppScreen.TurnierDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
@@ -284,10 +284,10 @@ private fun BreadcrumbContent(
is AppScreen.TurnierNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
@@ -300,10 +300,10 @@ private fun BreadcrumbContent(
is AppScreen.Billing -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
@@ -356,7 +356,7 @@ private fun BreadcrumbContent(
is AppScreen.FunktionaerVerwaltung -> {
BreadcrumbSeparator()
Text(
text = "Richter-Verwaltung",
text = "Funktionär-Verwaltung",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
@@ -4,7 +4,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Place
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -33,7 +36,7 @@ fun VeranstaltungProfilScreen(
val turniere = TurnierStore.list(veranstaltungId)
if (veranstaltung == null) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Veranstaltung nicht gefunden") }
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Event nicht gefunden") }
return@DesktopTheme
}
@@ -65,7 +68,7 @@ fun VeranstaltungProfilScreen(
KpiCard("Ort", veranstaltung.ort, Icons.Default.Place, Modifier.weight(1f))
}
Text("Turniere in dieser Veranstaltung", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text("Turniere in diesem Event", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
if (turniere.isEmpty()) {
Card(Modifier.fillMaxWidth()) {
Box(Modifier.padding(32.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
@@ -81,7 +84,7 @@ fun VeranstaltungProfilScreen(
}
}
// Rechte Spalte: Veranstalter Info & Aktionen
// Rechte Spalte: Veranstalter Information & Aktionen
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Card {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {