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:
+1
@@ -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(
|
||||
|
||||
+164
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+78
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user