Compare commits
2 Commits
19ba044ec0
...
d9b5c6bfea
| Author | SHA1 | Date | |
|---|---|---|---|
| d9b5c6bfea | |||
| 91a8c38b25 |
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package at.mocode.frontend.core.wizard.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desktop-spezifische Variante. Hotkeys werden in einem Folge‑Inkrement ergänzt,
|
||||||
|
* sobald die Compose‑API-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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user