diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationScreen.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationScreen.kt index 53b74b02..4e52c351 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationScreen.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -69,31 +70,45 @@ fun DeviceInitializationScreen( NetworkRoleSelector( selectedRole = uiState.settings.networkRole, onRoleSelected = { + if (uiState.settings.networkRole != it && uiState.settings.deviceName.isNotBlank()) { + // Hier könnte ein Dialog kommen, aber fürs Erste einfach setzen + } viewModel.setNetworkRole(it) focusManager.moveFocus(FocusDirection.Next) }, - modifier = Modifier.focusRequester(roleSelectorFocus) + modifier = Modifier.focusRequester(roleSelectorFocus), + enabled = !uiState.isLocked ) - Button( - onClick = { viewModel.nextStep() }, - modifier = Modifier - .align(Alignment.End) - .focusRequester(nextButtonFocus) - .onKeyEvent { - if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) { - viewModel.nextStep() - true - } else false - } - ) { - Text("Weiter") - Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null) + if (!uiState.isLocked) { + Button( + onClick = { viewModel.nextStep() }, + modifier = Modifier + .align(Alignment.End) + .focusRequester(nextButtonFocus) + .onKeyEvent { + if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) { + viewModel.nextStep() + true + } else false + } + ) { + Text("Weiter") + Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null) + } + } else { + Button( + onClick = { viewModel.nextStep() }, + modifier = Modifier.align(Alignment.End) + ) { + Text("Zur Konfiguration") + Icon(Icons.AutoMirrored.Filled.ArrowForward, null) + } } } } } else { - // PHASE 2: ROLLENSPEZIFISCH (JVM spezifische Implementierung folgt) + // PHASE 2 & Review DeviceInitializationConfig( uiState = uiState, viewModel = viewModel @@ -110,12 +125,24 @@ fun DeviceInitializationScreen( Text("Zurück zur Rollenauswahl") } - Button( - onClick = { viewModel.completeInitialization() }, - enabled = DeviceInitializationValidator.canContinue(uiState.settings) - ) { - Text("Konfiguration abschließen") - Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp)) + if (uiState.isLocked) { + Button( + onClick = { viewModel.unlockConfiguration() }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondary + ) + ) { + Text("Konfiguration bearbeiten") + Icon(Icons.Default.Edit, null, Modifier.padding(start = 8.dp)) + } + } else { + Button( + onClick = { viewModel.completeInitialization() }, + enabled = DeviceInitializationValidator.canContinue(uiState.settings) + ) { + Text("Konfiguration finalisieren & Sperren") + Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp)) + } } } } diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationUiState.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationUiState.kt index f6e2c072..cf9ac73d 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationUiState.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationUiState.kt @@ -10,5 +10,6 @@ data class DeviceInitializationUiState( val settings: DeviceInitializationSettings = DeviceInitializationSettings(), val discoveredMasters: List = emptyList(), val isProcessing: Boolean = false, - val error: String? = null + val error: String? = null, + val isLocked: Boolean = false ) diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt index 2b6bfd26..0ea2df8e 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt @@ -1,16 +1,14 @@ @file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") package at.mocode.frontend.features.device.initialization.presentation -import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings -import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole -import at.mocode.frontend.features.device.initialization.domain.model.ExpectedClient - - import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService +import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings +import at.mocode.frontend.features.device.initialization.domain.model.ExpectedClient +import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -75,7 +73,13 @@ class DeviceInitializationViewModel( } fun completeInitialization() { - println("[DeviceInit] Konfiguration abgeschlossen. Speichere Einstellungen...") + println("[DeviceInit] Konfiguration wird finalisiert...") + _uiState.update { it.copy(isLocked = true) } onInitializationComplete(_uiState.value.settings) } + + fun unlockConfiguration() { + println("[DeviceInit] Konfiguration entsperrt für Änderungen.") + _uiState.update { it.copy(isLocked = false) } + } } diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/NetworkRoleSelector.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/NetworkRoleSelector.kt index a8d95d99..f307e8c2 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/NetworkRoleSelector.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/NetworkRoleSelector.kt @@ -18,16 +18,18 @@ import at.mocode.frontend.features.device.initialization.domain.model.NetworkRol fun NetworkRoleSelector( selectedRole: NetworkRole, onRoleSelected: (NetworkRole) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + enabled: Boolean = true ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { NetworkRoleCard( title = "Master (Host)", description = "Verwaltet die zentrale Datenbank und koordiniert den Sync.", isSelected = selectedRole == NetworkRole.MASTER, - onClick = { onRoleSelected(NetworkRole.MASTER) }, + onClick = { if (enabled) onRoleSelected(NetworkRole.MASTER) }, + enabled = enabled, modifier = Modifier.onKeyEvent { - if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) { + if (enabled && (it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) { onRoleSelected(NetworkRole.MASTER) true } else false @@ -38,9 +40,10 @@ fun NetworkRoleSelector( title = "Client", description = "Verbindet sich mit einem Master-Gerät im lokalen Netzwerk.", isSelected = selectedRole == NetworkRole.CLIENT, - onClick = { onRoleSelected(NetworkRole.CLIENT) }, + onClick = { if (enabled) onRoleSelected(NetworkRole.CLIENT) }, + enabled = enabled, modifier = Modifier.onKeyEvent { - if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) { + if (enabled && (it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) { onRoleSelected(NetworkRole.CLIENT) true } else false @@ -55,24 +58,36 @@ private fun NetworkRoleCard( description: String, isSelected: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + enabled: Boolean = true ) { Surface( onClick = onClick, + enabled = enabled, shape = MaterialTheme.shapes.medium, - color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant, + color = when { + isSelected -> MaterialTheme.colorScheme.primaryContainer + !enabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + else -> MaterialTheme.colorScheme.surfaceVariant + }, modifier = modifier.fillMaxWidth() ) { Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { RadioButton( selected = isSelected, - onClick = null + onClick = null, + enabled = enabled ) Column { - Text(title, style = MaterialTheme.typography.labelLarge) + Text( + title, + style = MaterialTheme.typography.labelLarge, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) Text( description, - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall, + color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) ) } } diff --git a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt index 426314bf..45a6214d 100644 --- a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt +++ b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt @@ -60,7 +60,8 @@ actual fun DeviceInitializationConfig( errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.", imeAction = ImeAction.Next, keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }), - modifier = Modifier.focusRequester(deviceNameFocus) + modifier = Modifier.focusRequester(deviceNameFocus), + enabled = !uiState.isLocked ) var passwordVisible by remember { mutableStateOf(false) } @@ -71,14 +72,15 @@ actual fun DeviceInitializationConfig( placeholder = "Mindestens 8 Zeichen", isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey), errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.", - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + visualTransformation = if (passwordVisible || uiState.isLocked) VisualTransformation.None else PasswordVisualTransformation(), imeAction = ImeAction.Next, keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Next) } ), modifier = Modifier.focusRequester(sharedKeyFocus), trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, - onTrailingIconClick = { passwordVisible = !passwordVisible } + onTrailingIconClick = { passwordVisible = !passwordVisible }, + enabled = !uiState.isLocked ) MsFilePicker( @@ -88,7 +90,18 @@ actual fun DeviceInitializationConfig( viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) } }, directoryOnly = true, - modifier = Modifier.focusRequester(backupPathFocus) + modifier = Modifier.focusRequester(backupPathFocus), + enabled = !uiState.isLocked + ) + + MsTextField( + value = settings.defaultPrinter, + onValueChange = { viewModel.updateSettings { s -> s.copy(defaultPrinter = it) } }, + label = "Standard-Drucker", + placeholder = "z.B. Brother-HL-L2350DW", + enabled = !uiState.isLocked, + imeAction = ImeAction.Done, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) if (settings.networkRole == NetworkRole.MASTER) { @@ -97,21 +110,24 @@ actual fun DeviceInitializationConfig( value = settings.syncInterval.toFloat(), onValueChange = { viewModel.updateSettings { s -> s.copy(syncInterval = it.toInt()) } }, valueRange = 1f..60f, - steps = 59 + steps = 59, + enabled = !uiState.isLocked ) } else { // Button zum Abschließen für Clients, da diese keinen Slider/Clients haben Spacer(Modifier.height(8.dp)) - Button( - onClick = { viewModel.completeInitialization() }, - modifier = Modifier.fillMaxWidth(), - enabled = DeviceInitializationValidator.canContinue(settings) - ) { - Text("Konfiguration abschließen") + if (!uiState.isLocked) { + Button( + onClick = { viewModel.completeInitialization() }, + modifier = Modifier.fillMaxWidth(), + enabled = DeviceInitializationValidator.canContinue(settings) + ) { + Text("Konfiguration abschließen") + } } } - if (settings.networkRole == NetworkRole.MASTER) { + if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) { HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall) @@ -255,6 +271,18 @@ actual fun DeviceInitializationConfig( color = MaterialTheme.colorScheme.onSurfaceVariant ) } + if (settings.networkRole == NetworkRole.MASTER && uiState.isLocked) { + HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) + Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall) + settings.expectedClients.forEach { client -> + ListItem( + headlineContent = { Text(client.name) }, + trailingContent = { + SuggestionChip(onClick = {}, label = { Text(client.role.name) }) + } + ) + } + } } } } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt index 91d12a22..7f270465 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt @@ -1,8 +1,10 @@ package at.mocode.veranstaltung.feature.di import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel +import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel import org.koin.dsl.module val veranstaltungModule = module { factory { VeranstaltungManagementViewModel(get()) } + factory { VeranstaltungWizardViewModel(get(), get(), get()) } } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt new file mode 100644 index 00000000..15f717ab --- /dev/null +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt @@ -0,0 +1,285 @@ +package at.mocode.veranstaltung.feature.presentation + +import androidx.compose.foundation.background +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.material.icons.filled.Check +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import at.mocode.frontend.core.designsystem.theme.Dimens +import kotlin.uuid.ExperimentalUuidApi + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class) +@Composable +fun VeranstaltungWizardScreen( + viewModel: VeranstaltungWizardViewModel, + onBack: () -> Unit, + onFinish: () -> Unit +) { + val state = viewModel.state + + Scaffold( + topBar = { + Column { + TopAppBar( + title = { Text("Neue Veranstaltung anlegen") }, + navigationIcon = { + IconButton(onClick = { + if (state.currentStep == WizardStep.ZNS_CHECK) onBack() + else viewModel.previousStep() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Zurück") + } + } + ) + LinearProgressIndicator( + progress = { (state.currentStep.ordinal + 1).toFloat() / WizardStep.entries.size.toFloat() }, + modifier = Modifier.fillMaxWidth() + ) + } + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + // Sticky Preview Card + VorschauCard(state = state) + + Box( + modifier = Modifier + .fillMaxSize() + .padding(Dimens.SpacingL) + .verticalScroll(rememberScrollState()) + ) { + when (state.currentStep) { + WizardStep.ZNS_CHECK -> ZnsCheckStep(viewModel) + WizardStep.VERANSTALTER_SELECTION -> VeranstalterSelectionStep(viewModel) + WizardStep.ANSPRECHPERSON_MAPPING -> AnsprechpersonMappingStep(viewModel) + WizardStep.META_DATA -> MetaDataStep(viewModel) + WizardStep.TURNIER_ANLAGE -> TurnierAnlageStep(viewModel) + WizardStep.SUMMARY -> SummaryStep(viewModel, onFinish) + } + } + } + } +} + +@Composable +private fun VorschauCard(state: VeranstaltungWizardState) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(Dimens.SpacingM), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Row( + modifier = Modifier.padding(Dimens.SpacingM), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM) + ) { + // Placeholder für Logo + Box( + modifier = Modifier + .size(64.dp) + .background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.small), + contentAlignment = Alignment.Center + ) { + Text("LOGO", style = MaterialTheme.typography.labelSmall) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = state.name.ifBlank { "Neue Veranstaltung" }, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = state.veranstalterName.ifBlank { "Kein Veranstalter gewählt" }, + style = MaterialTheme.typography.bodyMedium + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = state.ort.ifBlank { "Ort noch nicht festgelegt" }, + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "| ${state.startDatum ?: ""} - ${state.endDatum ?: ""}", + style = MaterialTheme.typography.bodySmall + ) + } + } + } + } +} + +@Composable +private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) { + val state = viewModel.state + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 1: Stammdaten-Verfügbarkeit prüfen", style = MaterialTheme.typography.titleLarge) + + if (!state.isZnsAvailable) { + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { + Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Info, null, tint = MaterialTheme.colorScheme.error) + Spacer(Modifier.width(12.dp)) + Column { + Text("Stammdaten fehlen!", fontWeight = FontWeight.Bold) + Text("Bitte importieren Sie die aktuelle ZNS.zip über den ZNS-Importer.") + } + } + } + Button(onClick = { /* Navigiere zum ZNS Importer */ }) { + Icon(Icons.Default.CloudDownload, null) + Spacer(Modifier.width(8.dp)) + Text("ZNS-Importer öffnen") + } + } else { + Card(colors = CardDefaults.cardColors(containerColor = Color(0xFFE8F5E9))) { + Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Check, null, tint = Color(0xFF2E7D32)) + Spacer(Modifier.width(12.dp)) + Text("Stammdaten sind aktuell und verfügbar.", color = Color(0xFF2E7D32)) + } + } + Button(onClick = { viewModel.nextStep() }) { + Text("Weiter zur Veranstalter-Wahl") + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class) +@Composable +private fun VeranstalterSelectionStep(viewModel: VeranstaltungWizardViewModel) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 2: Veranstalter auswählen", style = MaterialTheme.typography.titleLarge) + Text("Suchen Sie nach dem Verein (Name oder OEPS-Nummer).") + + // Mock Suche + OutlinedTextField( + value = "", + onValueChange = {}, + label = { Text("Verein suchen...") }, + modifier = Modifier.fillMaxWidth() + ) + + Button(onClick = { + // Mock Selection + viewModel.setVeranstalter( + id = kotlin.uuid.Uuid.random(), + nummer = "6-009", + name = "Union Reit- u. Fahrverein Neumarkt/M.", + standardOrt = "4212 Neumarkt, Reitanlage Stroblmair", + logo = null + ) + viewModel.nextStep() + }) { + Text("Union Reit- u. Fahrverein Neumarkt/M. (6-009) wählen") + } + } +} + +@Composable +private fun AnsprechpersonMappingStep(viewModel: VeranstaltungWizardViewModel) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 3: Ansprechperson festlegen", style = MaterialTheme.typography.titleLarge) + Text("Wer ist für diese Veranstaltung verantwortlich?") + + Button(onClick = { + viewModel.setAnsprechperson("12345", "Ursula Stroblmair") + viewModel.nextStep() + }) { + Text("Ursula Stroblmair (aus Stammdaten) verknüpfen") + } + + OutlinedButton(onClick = { viewModel.nextStep() }) { + Text("Neue Person anlegen (Offline-Profil)") + } + } +} + +@Composable +private fun MetaDataStep(viewModel: VeranstaltungWizardViewModel) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 4: Veranstaltungs-Parameter", style = MaterialTheme.typography.titleLarge) + + OutlinedTextField( + value = viewModel.state.name, + onValueChange = { viewModel.updateMetaData(it, viewModel.state.ort, viewModel.state.startDatum, viewModel.state.endDatum, viewModel.state.logoUrl) }, + label = { Text("Name der Veranstaltung") }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = viewModel.state.ort, + onValueChange = { viewModel.updateMetaData(viewModel.state.name, it, viewModel.state.startDatum, viewModel.state.endDatum, viewModel.state.logoUrl) }, + label = { Text("Veranstaltungs-Ort") }, + modifier = Modifier.fillMaxWidth() + ) + + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + // Mock Date Picker + Button(onClick = { viewModel.nextStep() }) { + Text("Weiter zur Turnier-Anlage") + } + } + } +} + +@Composable +private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge) + Text("Fügen Sie die pferdesportlichen Veranstaltungen (Turniere) hinzu.") + + Card(modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(16.dp)) { + Text("Turnier #1", fontWeight = FontWeight.Bold) + Text("Turnier-Nr: (noch nicht vergeben)") + Button(onClick = {}) { + Text("Ausschreibung (PDF) hochladen") + } + } + } + + Button(onClick = { viewModel.nextStep() }) { + Text("Zusammenfassung") + } + } +} + +@Composable +private fun SummaryStep(viewModel: VeranstaltungWizardViewModel, onFinish: () -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 6: Zusammenfassung", style = MaterialTheme.typography.titleLarge) + + Card(modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(16.dp)) { + Text("Überprüfen Sie Ihre Angaben, bevor Sie die Veranstaltung anlegen.") + // ... Summary Details ... + } + } + + Button( + onClick = onFinish, + modifier = Modifier.fillMaxWidth() + ) { + Text("Veranstaltung jetzt anlegen") + } + } +} diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt index 78d4669c..5650e2ad 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt @@ -19,22 +19,31 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid enum class WizardStep { - ZNS_IMPORT, + ZNS_CHECK, + VERANSTALTER_SELECTION, + ANSPRECHPERSON_MAPPING, META_DATA, - TYPE_SELECTION + TURNIER_ANLAGE, + SUMMARY } @OptIn(ExperimentalUuidApi::class) data class VeranstaltungWizardState( - val currentStep: WizardStep = WizardStep.ZNS_IMPORT, + val currentStep: WizardStep = WizardStep.ZNS_CHECK, val veranstalterId: Uuid? = null, + val veranstalterVereinsNummer: String = "", + val veranstalterName: String = "", + val ansprechpersonSatznummer: String = "", + val ansprechpersonName: String = "", val name: String = "", val ort: String = "", val startDatum: LocalDate? = null, val endDatum: LocalDate? = null, + val logoUrl: String? = null, val isSaving: Boolean = false, val error: String? = null, - val createdVeranstaltungId: Uuid? = null + val createdVeranstaltungId: Uuid? = null, + val isZnsAvailable: Boolean = false ) @OptIn(ExperimentalUuidApi::class) @@ -47,12 +56,27 @@ class VeranstaltungWizardViewModel( var state by mutableStateOf(VeranstaltungWizardState()) private set + init { + checkZnsAvailability() + } + + fun checkZnsAvailability() { + // Hier prüfen wir, ob Stammdaten vorhanden sind (Simuliert) + viewModelScope.launch { + val hasData = true // Simulation: Stammdaten sind da + state = state.copy(isZnsAvailable = hasData) + } + } + fun nextStep() { state = state.copy( currentStep = when (state.currentStep) { - WizardStep.ZNS_IMPORT -> WizardStep.META_DATA - WizardStep.META_DATA -> WizardStep.TYPE_SELECTION - WizardStep.TYPE_SELECTION -> WizardStep.TYPE_SELECTION + WizardStep.ZNS_CHECK -> WizardStep.VERANSTALTER_SELECTION + WizardStep.VERANSTALTER_SELECTION -> WizardStep.ANSPRECHPERSON_MAPPING + WizardStep.ANSPRECHPERSON_MAPPING -> WizardStep.META_DATA + WizardStep.META_DATA -> WizardStep.TURNIER_ANLAGE + WizardStep.TURNIER_ANLAGE -> WizardStep.SUMMARY + WizardStep.SUMMARY -> WizardStep.SUMMARY } ) } @@ -60,19 +84,32 @@ class VeranstaltungWizardViewModel( fun previousStep() { state = state.copy( currentStep = when (state.currentStep) { - WizardStep.ZNS_IMPORT -> WizardStep.ZNS_IMPORT - WizardStep.META_DATA -> WizardStep.ZNS_IMPORT - WizardStep.TYPE_SELECTION -> WizardStep.META_DATA + WizardStep.ZNS_CHECK -> WizardStep.ZNS_CHECK + WizardStep.VERANSTALTER_SELECTION -> WizardStep.ZNS_CHECK + WizardStep.ANSPRECHPERSON_MAPPING -> WizardStep.VERANSTALTER_SELECTION + WizardStep.META_DATA -> WizardStep.ANSPRECHPERSON_MAPPING + WizardStep.TURNIER_ANLAGE -> WizardStep.META_DATA + WizardStep.SUMMARY -> WizardStep.TURNIER_ANLAGE } ) } - fun updateMetaData(name: String, ort: String, start: LocalDate?, end: LocalDate?) { - state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end) + fun setVeranstalter(id: Uuid, nummer: String, name: String, standardOrt: String, logo: String?) { + state = state.copy( + veranstalterId = id, + veranstalterVereinsNummer = nummer, + veranstalterName = name, + ort = standardOrt, + logoUrl = logo + ) } - fun setVeranstalter(id: Uuid) { - state = state.copy(veranstalterId = id) + fun setAnsprechperson(satznummer: String, name: String) { + state = state.copy(ansprechpersonSatznummer = satznummer, ansprechpersonName = name) + } + + fun updateMetaData(name: String, ort: String, start: LocalDate?, end: LocalDate?, logo: String?) { + state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end, logoUrl = logo) } fun saveVeranstaltung() { @@ -99,7 +136,7 @@ class VeranstaltungWizardViewModel( } if (response.status == HttpStatusCode.Created) { - // Hier müsste die ID aus dem Response gelesen werden, falls benötigt + // Hier müsste die ID aus der Response gelesen werden, falls benötigt state = state.copy(isSaving = false) nextStep() } else { diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt index d26bc6e4..a56d2f34 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt @@ -45,7 +45,6 @@ import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWiz import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen -import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen import at.mocode.veranstaltung.feature.presentation.VeranstaltungenScreen import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel @@ -235,9 +234,11 @@ fun DesktopContentArea( } is AppScreen.VeranstaltungNeu -> { - VeranstaltungNeuScreen( + val viewModel: at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel = koinViewModel() + at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardScreen( + viewModel = viewModel, onBack = onBack, - onSave = { onBack() } + onFinish = { onBack() } ) }