Compare commits

...

2 Commits

Author SHA1 Message Date
d9b5c6bfea feat: aktiviere neues EventWizardScreen-Scaffold hinter Feature-Flag
Some checks failed
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:58:48 +02:00
91a8c38b25 feat: implementiere WizardScaffold und Hotkey-Integration mittels Compose
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:56:56 +02:00
4 changed files with 238 additions and 0 deletions

View File

@ -5,6 +5,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
} }
group = "at.mocode.frontend.core" group = "at.mocode.frontend.core"
@ -29,6 +31,11 @@ kotlin {
implementation(projects.frontend.core.domain) implementation(projects.frontend.core.domain)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
// Compose (for WizardScaffold UI in commonMain)
implementation(compose.ui)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
} }
commonTest.dependencies { commonTest.dependencies {
@ -38,6 +45,8 @@ kotlin {
jvmMain.dependencies { jvmMain.dependencies {
implementation(projects.frontend.core.navigation) implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.domain) implementation(projects.frontend.core.domain)
implementation(compose.ui)
implementation(compose.materialIconsExtended)
} }
jvmTest.dependencies { jvmTest.dependencies {

View File

@ -0,0 +1,109 @@
package at.mocode.frontend.core.wizard.ui
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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
data class WizardStepUi(
val id: String,
val title: String,
val subtitle: String? = null,
val isSkipped: Boolean = false,
val enabled: Boolean = true
)
@Composable
fun WizardScaffold(
steps: List<WizardStepUi>,
currentIndex: Int,
canBack: Boolean,
canNext: Boolean,
onBack: () -> Unit,
onNext: () -> Unit,
onSaveDraft: (() -> Unit)? = null,
nextLabel: String = "Weiter",
backLabel: String = "Zurück",
finishLabel: String = "Fertig",
content: @Composable () -> Unit
) {
val isLast = currentIndex >= steps.lastIndex && steps.isNotEmpty()
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Header: Breadcrumb / Step-Leiste
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
steps.forEachIndexed { idx, meta ->
val color = when {
idx == currentIndex -> MaterialTheme.colorScheme.primary
meta.isSkipped -> MaterialTheme.colorScheme.secondary
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "${idx + 1}.",
color = color,
style = MaterialTheme.typography.labelMedium,
fontWeight = if (idx == currentIndex) FontWeight.Bold else FontWeight.Normal
)
Text(
text = " ${meta.title}" + if (meta.isSkipped) " (übersprungen)" else "",
color = color,
style = MaterialTheme.typography.labelMedium
)
}
if (idx != steps.lastIndex) {
Text(text = " ", color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f))
}
}
}
// Content
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
content()
}
Spacer(Modifier.height(12.dp))
// Footer: Actions
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedButton(onClick = onBack, enabled = canBack) {
Text(backLabel)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (onSaveDraft != null) {
OutlinedButton(onClick = onSaveDraft) {
Text("Zwischenspeichern (Alt+S)")
}
Spacer(Modifier.padding(horizontal = 6.dp))
}
Button(onClick = onNext, enabled = canNext) {
Text(if (isLast) finishLabel else nextLabel)
}
}
}
}
}
}

View File

@ -0,0 +1,36 @@
package at.mocode.frontend.core.wizard.ui
import androidx.compose.runtime.Composable
/**
* Desktop-spezifische Variante. Hotkeys werden in einem FolgeInkrement ergänzt,
* sobald die ComposeAPI-Version projektweit abgestimmt ist.
*/
@Composable
fun WizardScaffoldWithHotkeys(
steps: List<WizardStepUi>,
currentIndex: Int,
canBack: Boolean,
canNext: Boolean,
onBack: () -> Unit,
onNext: () -> Unit,
onSaveDraft: (() -> Unit)? = null,
nextLabel: String = "Weiter",
backLabel: String = "Zurück",
finishLabel: String = "Fertig",
content: @Composable () -> Unit
) {
WizardScaffold(
steps = steps,
currentIndex = currentIndex,
canBack = canBack,
canNext = canNext,
onBack = onBack,
onNext = onNext,
onSaveDraft = onSaveDraft,
nextLabel = nextLabel,
backLabel = backLabel,
finishLabel = finishLabel,
content = content
)
}

View File

@ -17,6 +17,9 @@ 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.core.domain.config.WizardFeatureFlags
import at.mocode.frontend.core.wizard.ui.WizardScaffoldWithHotkeys
import at.mocode.frontend.core.wizard.ui.WizardStepUi
import at.mocode.frontend.features.turnier.presentation.TurnierWizard import at.mocode.frontend.features.turnier.presentation.TurnierWizard
import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.ExperimentalUuidApi
@ -31,6 +34,31 @@ fun EventWizardScreen(
) { ) {
val state = viewModel.state val state = viewModel.state
// Neuer Scaffold-Weg hinter Feature-Flag (Strangler-Pattern)
if (WizardFeatureFlags.WizardRuntimeEnabled) {
EventWizardScreenScaffolded(
state = state,
onBack = {
if (state.currentStep == WizardStep.ZNS_CHECK) onBack() else viewModel.previousStep()
},
onNext = { viewModel.nextStep() },
onSaveDraft = null, // Wird in einem Folge-Inkrement angebunden
onFinish = onFinish,
onNavigateToVeranstalterNeu = onNavigateToVeranstalterNeu,
renderStep = {
when (state.currentStep) {
WizardStep.ZNS_CHECK -> ZnsCheckStep(viewModel)
WizardStep.VERANSTALTER_SELECTION -> VeranstalterSelectionStep(viewModel, onNavigateToVeranstalterNeu)
WizardStep.ANSPRECHPERSON_MAPPING -> AnsprechpersonMappingStep(viewModel)
WizardStep.META_DATA -> MetaDataStep(viewModel)
WizardStep.TURNIER_ANLAGE -> TurnierAnlageStep(viewModel)
WizardStep.SUMMARY -> SummaryStep(viewModel, onFinish)
}
}
)
return
}
Scaffold( Scaffold(
topBar = { topBar = {
Column { Column {
@ -79,6 +107,62 @@ fun EventWizardScreen(
} }
} }
@Composable
private fun EventWizardScreenScaffolded(
state: VeranstaltungWizardState,
onBack: () -> Unit,
onNext: () -> Unit,
onSaveDraft: (() -> Unit)?,
onFinish: () -> Unit,
onNavigateToVeranstalterNeu: () -> Unit,
renderStep: @Composable () -> Unit
) {
val steps = remember {
WizardStep.entries.map {
// Titel schlank aus Enum ableiten; echte Strings folgen in UI-Polishing
val title = when (it) {
WizardStep.ZNS_CHECK -> "ZNS"
WizardStep.VERANSTALTER_SELECTION -> "Veranstalter"
WizardStep.ANSPRECHPERSON_MAPPING -> "Kontakt"
WizardStep.META_DATA -> "Metadaten"
WizardStep.TURNIER_ANLAGE -> "Turniere"
WizardStep.SUMMARY -> "Zusammenfassung"
}
WizardStepUi(id = it.name, title = title)
}
}
val currentIndex = state.currentStep.ordinal
val canBack = currentIndex > 0
val canNext = true // Validierungslogik wird schrittweise ergänzt
WizardScaffoldWithHotkeys(
steps = steps,
currentIndex = currentIndex,
canBack = canBack,
canNext = canNext,
onBack = onBack,
onNext = if (state.currentStep == WizardStep.SUMMARY) onFinish else onNext,
onSaveDraft = onSaveDraft,
nextLabel = if (state.currentStep == WizardStep.SUMMARY) "Fertig" else "Weiter",
backLabel = "Zurück",
finishLabel = "Fertig"
) {
// Sticky Preview oben wie gehabt
Column(modifier = Modifier.fillMaxSize()) {
VorschauCard(state = state)
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(Dimens.SpacingL)
) {
renderStep()
}
}
}
}
@Composable @Composable
private fun VorschauCard(state: VeranstaltungWizardState) { private fun VorschauCard(state: VeranstaltungWizardState) {
Card( Card(