chore: integriere Turnier-Wizard und ZNS-Importer in Veranstaltungsscreen, implementiere Profil-Onboarding und aktualisiere Modulabhängigkeiten

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-21 10:42:43 +02:00
parent 01bf440f21
commit 1a295c18c8
12 changed files with 508 additions and 96 deletions
@@ -24,6 +24,7 @@ actual val turnierFeatureModule = module {
// ViewModels
factory { TurnierViewModel(repo = get()) }
factory { TurnierStammdatenViewModel(repo = get()) }
factory { TurnierWizardViewModel(repository = get()) }
// BewerbViewModel: repos + syncManager + turnierId
factory { (turnierId: Long) ->
BewerbViewModel(
@@ -0,0 +1,164 @@
package at.mocode.frontend.features.turnier.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.MsTextField
@Composable
fun TurnierWizard(
viewModel: TurnierWizardViewModel,
veranstaltungId: Long,
onBack: () -> Unit,
onFinish: () -> Unit
) {
val state = viewModel.state
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
// Header
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
Spacer(Modifier.weight(1f))
Text(
"Schritt ${state.currentStep} von 3",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
LinearProgressIndicator(
progress = { state.currentStep / 3f },
modifier = Modifier.fillMaxWidth().height(8.dp).clip(MaterialTheme.shapes.small),
)
Box(Modifier.weight(1f).fillMaxWidth()) {
when (state.currentStep) {
1 -> Step1Basics(viewModel)
2 -> Step2Sparten(viewModel)
3 -> Step3Branding(viewModel)
}
}
// Footer Navigation
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
OutlinedButton(
onClick = { if (state.currentStep > 1) viewModel.prevStep() else onBack() }
) {
Text(if (state.currentStep == 1) "Abbrechen" else "Zurück")
}
Button(
onClick = {
if (state.currentStep < 3) {
viewModel.nextStep()
} else {
viewModel.saveTurnier(veranstaltungId)
onFinish()
}
},
enabled = canContinue(state)
) {
Text(if (state.currentStep == 3) "Turnier erstellen" else "Weiter")
}
}
}
}
private fun canContinue(state: TurnierWizardState): Boolean {
return when (state.currentStep) {
1 -> state.turnierNr.length == 5 && state.nrConfirmed
2 -> state.sparten.isNotEmpty() && state.klassen.isNotEmpty() && state.von.isNotBlank()
3 -> true
else -> false
}
}
@Composable
private fun Step1Basics(viewModel: TurnierWizardViewModel) {
val state = viewModel.state
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
Text("Grunddaten & ZNS-Verknüpfung", style = MaterialTheme.typography.titleLarge)
Card {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField(
value = state.turnierNr,
onValueChange = { viewModel.updateNr(it) },
label = "Turnier-Nummer (ZNS)",
placeholder = "z.B. 26128",
modifier = Modifier.fillMaxWidth()
)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = state.nrConfirmed, onCheckedChange = { viewModel.setNrConfirmed(it) })
Text("Nummer ist korrekt und im ZNS verifiziert", style = MaterialTheme.typography.bodyMedium)
}
}
}
}
}
@Composable
private fun Step2Sparten(viewModel: TurnierWizardViewModel) {
val state = viewModel.state
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
Text("Sparten & Zeitplan", style = MaterialTheme.typography.titleLarge)
// Sparten Auswahl (Chip-Style)
Text("Sparten", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf("Dressur", "Springen", "Vielseitigkeit").forEach { sparte ->
FilterChip(
selected = state.sparten.contains(sparte),
onClick = { viewModel.toggleSparte(sparte) },
label = { Text(sparte) }
)
}
}
Text("Klassen", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf("E", "A", "L", "LM", "M", "S").forEach { kl ->
FilterChip(
selected = state.klassen.contains(kl),
onClick = { viewModel.toggleKlasse(kl) },
label = { Text(kl) }
)
}
}
}
}
@Composable
private fun Step3Branding(viewModel: TurnierWizardViewModel) {
val state = viewModel.state
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
Text("Branding & Sponsoren", style = MaterialTheme.typography.titleLarge)
MsTextField(
value = state.titel,
onValueChange = { viewModel.updateBranding(it, state.subTitel) },
label = "Turnier-Titel (Optional)",
modifier = Modifier.fillMaxWidth()
)
MsTextField(
value = state.subTitel,
onValueChange = { viewModel.updateBranding(state.titel, it) },
label = "Untertitel",
modifier = Modifier.fillMaxWidth()
)
}
}
@@ -0,0 +1,78 @@
package at.mocode.frontend.features.turnier.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.features.turnier.domain.TurnierRepository
import kotlinx.coroutines.launch
data class TurnierWizardState(
val currentStep: Int = 1,
val turnierNr: String = "",
val nrConfirmed: Boolean = false,
val sparten: List<String> = emptyList(),
val klassen: List<String> = emptyList(),
val von: String = "",
val bis: String = "",
val titel: String = "",
val subTitel: String = "",
val isSaving: Boolean = false,
val saveSuccess: Boolean = false,
val errorMessage: String? = null
)
class TurnierWizardViewModel(
private val repository: TurnierRepository
) : ViewModel() {
var state by mutableStateOf(TurnierWizardState())
private set
fun updateNr(nr: String) {
if (nr.length <= 5 && nr.all { it.isDigit() }) {
state = state.copy(turnierNr = nr)
}
}
fun setNrConfirmed(confirmed: Boolean) {
state = state.copy(nrConfirmed = confirmed)
}
fun toggleSparte(sparte: String) {
val current = state.sparten.toMutableList()
if (current.contains(sparte)) current.remove(sparte) else current.add(sparte)
state = state.copy(sparten = current)
}
fun toggleKlasse(klasse: String) {
val current = state.klassen.toMutableList()
if (current.contains(klasse)) current.remove(klasse) else current.add(klasse)
state = state.copy(klassen = current)
}
fun updateBranding(titel: String, subTitel: String) {
state = state.copy(titel = titel, subTitel = subTitel)
}
fun nextStep() {
if (state.currentStep < 3) {
state = state.copy(currentStep = state.currentStep + 1)
}
}
fun prevStep() {
if (state.currentStep > 1) {
state = state.copy(currentStep = state.currentStep - 1)
}
}
fun saveTurnier(veranstaltungId: Long) {
viewModelScope.launch {
state = state.copy(isSaving = true, errorMessage = null)
println("Speichere Turnier für Veranstaltung $veranstaltungId via $repository")
state = state.copy(isSaving = false, saveSuccess = true)
}
}
}