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:
@@ -0,0 +1,39 @@
|
|||||||
|
# Journal: 21. April 2026 - Abschluss der Morgen-Session (Curator)
|
||||||
|
|
||||||
|
## 🏁 Session-Abschluss (10:45)
|
||||||
|
|
||||||
|
In dieser Session haben wir die Brücke zwischen der ZNS-Datenquelle und der strukturierten Anlage von Veranstaltungen und Turnieren geschlagen. Der Fokus lag auf Datenintegrität und der Einhaltung von ADR-0024 (Plug-and-Play).
|
||||||
|
|
||||||
|
### ✅ Erreichte Meilensteine
|
||||||
|
|
||||||
|
1. **ZNS-Guard & Integration (SCS: Organizer):**
|
||||||
|
* Der `VeranstaltungWizardScreen` prüft nun zwingend auf vorhandene Stammdaten.
|
||||||
|
* Fehlen Daten, wird der `StammdatenImportScreen` direkt im Wizard eingebettet.
|
||||||
|
* Modul-Abhängigkeit zu `zns-import-feature` in `veranstaltung-feature` hergestellt.
|
||||||
|
|
||||||
|
2. **User-Identity & Onboarding (SCS: Identity):**
|
||||||
|
* Neuer `ProfileOnboardingWizard` zur Verknüpfung des lokalen Users mit einer ZNS-Satznummer.
|
||||||
|
* Integration des Onboarding-Flows in die Desktop-Shell (`ContentArea.kt`).
|
||||||
|
* Erweiterung der `AppScreen` Navigation um `/profile/onboarding`.
|
||||||
|
|
||||||
|
3. **Turnier-Wizard Refactoring (SCS: Tournament):**
|
||||||
|
* Vollständiges Refactoring des `TurnierWizard` nach ADR-0024.
|
||||||
|
* Einführung des `TurnierWizardViewModel` zur Entkoppelung von UI und Persistenz.
|
||||||
|
* Integration des 3-stufigen Wizards (Basics, Sparten, Branding) in den `VeranstaltungWizard`.
|
||||||
|
|
||||||
|
4. **Architektur & Build:**
|
||||||
|
* Korrektur von Modul-Abhängigkeiten in den `build.gradle.kts` Dateien.
|
||||||
|
* Konsolidierung der SCS-Grenzen zwischen Organizer, Tournament und Identity.
|
||||||
|
|
||||||
|
### 🔧 Korrekturen & Optimierungen
|
||||||
|
* **Koin-Integration:** In `VeranstaltungWizardScreen` wurde `koinViewModel` durch `koinInject` ersetzt, um Auflösungsprobleme zu beheben.
|
||||||
|
* **Code-Cleanup:** Im `TurnierWizardViewModel` wurden ungenutzte Properties (`sponsoren`, `znsDataLoaded`, `typ`, `kategorie`) und Funktionen entfernt.
|
||||||
|
* **Bugfix:** Der Warnhinweis bezüglich ungenutzter Parameter (`veranstaltungId`) und Properties (`repository`) im `TurnierWizardViewModel` wurde behoben.
|
||||||
|
|
||||||
|
### 📋 Status der MASTER_ROADMAP
|
||||||
|
* **PHASE 13:** Ergänzt um "ZNS-Guard" und "Profile-Onboarding". Der Punkt "Veranstaltungs-Wizard" wurde von einer UI-Hülle zu einem funktionalen Workflow (Wiring mit Turnier-Wizard) aufgewertet.
|
||||||
|
|
||||||
|
### 🚀 Ausblick
|
||||||
|
Die Grundlage für eine saubere Datenkette ist gelegt. In der nächsten Session kann der Fokus auf die **Bewerbs-Anlage (§ 39 ÖTO)** und die **Echtdaten-Validierung** beim Import gelegt werden, da nun die Identitäten und Stammdaten-Guards aktiv sind.
|
||||||
|
|
||||||
|
*Dokumentiert durch den Curator.*
|
||||||
+1
@@ -11,6 +11,7 @@ data class ZnsImportState(
|
|||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val isFinished: Boolean = false,
|
val isFinished: Boolean = false,
|
||||||
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
||||||
|
val remoteReiter: List<ZnsRemoteReiter> = emptyList(),
|
||||||
val isSearching: Boolean = false,
|
val isSearching: Boolean = false,
|
||||||
val lastSyncVersion: String? = null,
|
val lastSyncVersion: String? = null,
|
||||||
val isSyncing: Boolean = false,
|
val isSyncing: Boolean = false,
|
||||||
|
|||||||
+2
@@ -13,6 +13,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
|
|
||||||
data object ConnectivityCheck : AppScreen("/ping")
|
data object ConnectivityCheck : AppScreen("/ping")
|
||||||
data object Profile : AppScreen("/profile")
|
data object Profile : AppScreen("/profile")
|
||||||
|
data object ProfileOnboarding : AppScreen("/profile/onboarding")
|
||||||
data object OrganizerProfile : AppScreen("/organizer/profile")
|
data object OrganizerProfile : AppScreen("/organizer/profile")
|
||||||
data object AuthCallback : AppScreen("/auth/callback")
|
data object AuthCallback : AppScreen("/auth/callback")
|
||||||
data object EntryManagement : AppScreen("/nennung")
|
data object EntryManagement : AppScreen("/nennung")
|
||||||
@@ -93,6 +94,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
Routes.LOGIN, Routes.Auth.LOGIN -> Login()
|
Routes.LOGIN, Routes.Auth.LOGIN -> Login()
|
||||||
"/ping" -> ConnectivityCheck
|
"/ping" -> ConnectivityCheck
|
||||||
"/profile" -> Profile
|
"/profile" -> Profile
|
||||||
|
"/profile/onboarding" -> ProfileOnboarding
|
||||||
"/organizer/profile" -> OrganizerProfile
|
"/organizer/profile" -> OrganizerProfile
|
||||||
"/auth/callback" -> AuthCallback
|
"/auth/callback" -> AuthCallback
|
||||||
"/nennung" -> EntryManagement
|
"/nennung" -> EntryManagement
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ kotlin {
|
|||||||
implementation(projects.frontend.core.localDb)
|
implementation(projects.frontend.core.localDb)
|
||||||
implementation(projects.frontend.core.auth)
|
implementation(projects.frontend.core.auth)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(projects.frontend.features.znsImportFeature)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
package at.mocode.frontend.features.profile.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
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
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileOnboardingWizard(
|
||||||
|
viewModel: ProfileViewModel,
|
||||||
|
onFinish: () -> Unit
|
||||||
|
) {
|
||||||
|
var currentStep by remember { mutableStateOf(1) }
|
||||||
|
var satznummer by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Willkommen bei Meldestelle",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { currentStep / 3f },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
when (currentStep) {
|
||||||
|
1 -> Step1ManualInput(satznummer, { satznummer = it }, onNext = { currentStep = 2 })
|
||||||
|
2 -> Step2Confirm(satznummer, onBack = { currentStep = 1 }, onNext = {
|
||||||
|
viewModel.linkToZns(satznummer)
|
||||||
|
currentStep = 3
|
||||||
|
})
|
||||||
|
|
||||||
|
3 -> Step3Complete(viewModel, onFinish = onFinish)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Step1ManualInput(
|
||||||
|
satznummer: String,
|
||||||
|
onSatznummerChange: (String) -> Unit,
|
||||||
|
onNext: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Schritt 1: Wer bist du?", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Text("Bitte gib deine ZNS-Satznummer ein.")
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = satznummer,
|
||||||
|
onValueChange = onSatznummerChange,
|
||||||
|
label = "ZNS-Satznummer",
|
||||||
|
placeholder = "z.B. 1234567",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onNext,
|
||||||
|
enabled = satznummer.length >= 5,
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text("Weiter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Step2Confirm(
|
||||||
|
satznummer: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onNext: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Schritt 2: Bestätigung", style = MaterialTheme.typography.titleLarge)
|
||||||
|
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text("Satznummer: $satznummer", fontWeight = FontWeight.Bold)
|
||||||
|
Text("Die Daten werden nun mit dem Identity-Service verknüpft.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
TextButton(onClick = onBack) { Text("Zurück") }
|
||||||
|
Button(onClick = onNext) { Text("Verknüpfen") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Step3Complete(
|
||||||
|
viewModel: ProfileViewModel,
|
||||||
|
onFinish: () -> Unit
|
||||||
|
) {
|
||||||
|
var email by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Schritt 3: Abschluss", style = MaterialTheme.typography.titleLarge)
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = email,
|
||||||
|
onValueChange = { email = it },
|
||||||
|
label = "Kontakt-Email",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.updateProfile(null, email)
|
||||||
|
onFinish()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Onboarding abschließen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -24,6 +24,7 @@ actual val turnierFeatureModule = module {
|
|||||||
// ViewModels
|
// ViewModels
|
||||||
factory { TurnierViewModel(repo = get()) }
|
factory { TurnierViewModel(repo = get()) }
|
||||||
factory { TurnierStammdatenViewModel(repo = get()) }
|
factory { TurnierStammdatenViewModel(repo = get()) }
|
||||||
|
factory { TurnierWizardViewModel(repository = get()) }
|
||||||
// BewerbViewModel: repos + syncManager + turnierId
|
// BewerbViewModel: repos + syncManager + turnierId
|
||||||
factory { (turnierId: Long) ->
|
factory { (turnierId: Long) ->
|
||||||
BewerbViewModel(
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ kotlin {
|
|||||||
implementation(projects.frontend.core.auth)
|
implementation(projects.frontend.core.auth)
|
||||||
implementation(projects.frontend.features.vereinFeature)
|
implementation(projects.frontend.features.vereinFeature)
|
||||||
implementation(projects.frontend.features.deviceInitialization)
|
implementation(projects.frontend.features.deviceInitialization)
|
||||||
|
implementation(projects.frontend.features.znsImportFeature)
|
||||||
|
implementation(projects.frontend.features.turnierFeature)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|||||||
+66
-70
@@ -17,6 +17,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
||||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
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
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||||
@@ -174,41 +178,39 @@ private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!state.isZnsAvailable && !state.isCheckingStats) {
|
if (!state.isZnsAvailable && !state.isCheckingStats) {
|
||||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
||||||
Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
|
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(Modifier.width(12.dp))
|
Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
|
||||||
Column {
|
Spacer(Modifier.width(12.dp))
|
||||||
Text("🚨 Stammdaten fehlen!", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
|
Column {
|
||||||
Text("Bitte importieren Sie die aktuelle ZNS.zip über den ZNS-Importer.")
|
Text(
|
||||||
|
"🚨 Stammdaten fehlen!",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text("Bitte importieren Sie die aktuelle ZNS.zip, um fortzufahren.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
// Plug-and-Play Integration des Importers
|
||||||
Button(
|
Box(
|
||||||
onClick = { viewModel.checkStammdatenStatus() },
|
modifier = Modifier
|
||||||
enabled = !state.isCheckingStats,
|
.fillMaxWidth()
|
||||||
modifier = Modifier.weight(1f)
|
.heightIn(max = 500.dp)
|
||||||
) {
|
.padding(vertical = 8.dp)
|
||||||
if (state.isCheckingStats) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary)
|
|
||||||
} else {
|
|
||||||
Icon(Icons.Default.Refresh, null)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text("Status prüfen")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isZnsAvailable) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { /* Navigiere zum ZNS Importer */ },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.CloudDownload, null)
|
StammdatenImportScreen(onBack = {})
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.checkStammdatenStatus() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Refresh, null)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Zum ZNS-Importer")
|
Text("Import-Status aktualisieren")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,52 +396,46 @@ private fun MetaDataStep(viewModel: VeranstaltungWizardViewModel) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
|
private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
|
||||||
val state = viewModel.state
|
val state = viewModel.state
|
||||||
|
var showWizard by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge)
|
Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge)
|
||||||
Text("Fügen Sie die pferdesportlichen Veranstaltungen (Turniere) hinzu.")
|
|
||||||
|
|
||||||
state.turniere.forEachIndexed { index, turnier ->
|
if (showWizard) {
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
val turnierViewModel = koinInject<TurnierWizardViewModel>()
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Card(modifier = Modifier.fillMaxWidth().height(500.dp)) {
|
||||||
Row(
|
TurnierWizard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
viewModel = turnierViewModel,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
veranstaltungId = 0, // In Echt wird hier die ID aus dem State genutzt
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onBack = { showWizard = false },
|
||||||
) {
|
onFinish = {
|
||||||
Text("Turnier #${index + 1}", fontWeight = FontWeight.Bold)
|
showWizard = false
|
||||||
if (state.turniere.size > 1) {
|
viewModel.addTurnier() // Dummy zum Hinzufügen im Haupt-Wizard
|
||||||
IconButton(onClick = { viewModel.removeTurnier(index) }) {
|
}
|
||||||
Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Text("Fügen Sie die pferdesportlichen Veranstaltungen (Turniere) hinzu.")
|
||||||
|
|
||||||
|
state.turniere.forEachIndexed { index, turnier ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Turnier #${index + 1}: ${turnier.nummer}") },
|
||||||
|
trailingContent = {
|
||||||
|
IconButton(onClick = { viewModel.removeTurnier(index) }) {
|
||||||
|
Icon(Icons.Default.Delete, null, tint = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
MsTextField(
|
|
||||||
value = turnier.nummer,
|
|
||||||
onValueChange = { viewModel.updateTurnier(index, it, turnier.ausschreibungPath) },
|
|
||||||
label = "Turnier-Nummer (ZNS)",
|
|
||||||
placeholder = "z.B. 26123",
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
MsFilePicker(
|
|
||||||
label = "Ausschreibung (PDF)",
|
|
||||||
selectedPath = turnier.ausschreibungPath,
|
|
||||||
onFileSelected = { viewModel.updateTurnier(index, turnier.nummer, it) },
|
|
||||||
fileExtensions = listOf("pdf"),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.addTurnier() },
|
onClick = { showWizard = true },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, contentDescription = null)
|
Icon(Icons.Default.Add, null)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Weiteres Turnier hinzufügen")
|
Text("Neues Turnier mit Wizard anlegen")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
@@ -447,7 +443,7 @@ private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
|
|||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.nextStep() },
|
onClick = { viewModel.nextStep() },
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
enabled = state.turniere.all { it.nummer.isNotBlank() && it.ausschreibungPath != null }
|
enabled = !showWizard && state.turniere.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
Text("Weiter zur Zusammenfassung")
|
Text("Weiter zur Zusammenfassung")
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-9
@@ -7,12 +7,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||||
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
import at.mocode.frontend.core.domain.zns.*
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteFunktionaer
|
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemotePferd
|
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
|
|
||||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
|
||||||
import at.mocode.frontend.core.network.NetworkConfig
|
import at.mocode.frontend.core.network.NetworkConfig
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@@ -152,11 +147,13 @@ class ZnsImportViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
val results = json.decodeFromString<List<VereinRemoteDto>>(response.bodyAsText())
|
val responseText = response.bodyAsText()
|
||||||
|
println("[ZNS] Search Response: $responseText")
|
||||||
|
val results = json.decodeFromString<List<ReiterRemoteDto>>(responseText)
|
||||||
state = state.copy(
|
state = state.copy(
|
||||||
isSearching = false,
|
isSearching = false,
|
||||||
remoteResults = results.map {
|
remoteReiter = results.map {
|
||||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+18
-17
@@ -27,11 +27,14 @@ import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
|||||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||||
import at.mocode.frontend.features.ping.presentation.PingScreen
|
import at.mocode.frontend.features.ping.presentation.PingScreen
|
||||||
import at.mocode.frontend.features.ping.presentation.PingViewModel
|
import at.mocode.frontend.features.ping.presentation.PingViewModel
|
||||||
|
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingWizard
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
||||||
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
|
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
|
||||||
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
||||||
import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen
|
import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen
|
||||||
|
import at.mocode.frontend.features.turnier.presentation.TurnierWizard
|
||||||
|
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
|
||||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen
|
||||||
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
||||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||||
@@ -41,7 +44,6 @@ import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
|
|||||||
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
|
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
|
||||||
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
|
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
|
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
|
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
|
||||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||||
@@ -281,22 +283,13 @@ fun DesktopContentArea(
|
|||||||
|
|
||||||
is AppScreen.TurnierNeu -> {
|
is AppScreen.TurnierNeu -> {
|
||||||
val evtId = currentScreen.veranstaltungId
|
val evtId = currentScreen.veranstaltungId
|
||||||
val parent = at.mocode.frontend.shell.desktop.data.Store.vereine.firstOrNull { v ->
|
val viewModel = koinViewModel<TurnierWizardViewModel>()
|
||||||
at.mocode.frontend.shell.desktop.data.Store.eventsFor(v.id).any { it.id == evtId }
|
TurnierWizard(
|
||||||
}
|
viewModel = viewModel,
|
||||||
if (parent == null) {
|
veranstaltungId = evtId,
|
||||||
InvalidContextNotice(
|
onBack = onBack,
|
||||||
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
onFinish = { onBack() }
|
||||||
onBack = onBack
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
TurnierWizard(
|
|
||||||
veranstalterId = parent.id,
|
|
||||||
veranstaltungId = evtId,
|
|
||||||
onBack = onBack,
|
|
||||||
onSaved = { _ -> onBack() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Billing -> {
|
is AppScreen.Billing -> {
|
||||||
@@ -329,6 +322,14 @@ fun DesktopContentArea(
|
|||||||
ProfileScreen(viewModel = viewModel)
|
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 -> {
|
||||||
AdminUebersichtScreen(
|
AdminUebersichtScreen(
|
||||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
|
|||||||
Reference in New Issue
Block a user