From 237c71e5a08c42ce1fdaa98a60c03673d4bcc5cb Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Tue, 21 Apr 2026 17:01:23 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20implementiere=20Wizard-Framework=20mit?= =?UTF-8?q?=20State-=20und=20Flow-Logik=20sowie=20Feature-Flags=20f=C3=BCr?= =?UTF-8?q?=20Migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: StefanMoCoAt --- .../core/domain/config/WizardFeatureFlags.kt | 10 +++ .../frontend/core/wizard/dsl/WizardDsl.kt | 81 +++++++++++++++++++ .../core/wizard/runtime/WizardCore.kt | 41 ++++++++++ .../core/wizard/samples/EventFlowSample.kt | 30 +++++++ 4 files changed, 162 insertions(+) create mode 100644 frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/config/WizardFeatureFlags.kt create mode 100644 frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/dsl/WizardDsl.kt create mode 100644 frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/runtime/WizardCore.kt create mode 100644 frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/samples/EventFlowSample.kt diff --git a/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/config/WizardFeatureFlags.kt b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/config/WizardFeatureFlags.kt new file mode 100644 index 00000000..a02e38bd --- /dev/null +++ b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/config/WizardFeatureFlags.kt @@ -0,0 +1,10 @@ +package at.mocode.frontend.core.domain.config + +/** + * Zentrale Feature-Flags für die Wizard-Orchestrator-Migration. + * Standard: AUS, damit bestehende Logik aktiv bleibt. + */ +object WizardFeatureFlags { + // Kann später via Settings/DI überschrieben werden + var WizardRuntimeEnabled: Boolean = false +} diff --git a/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/dsl/WizardDsl.kt b/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/dsl/WizardDsl.kt new file mode 100644 index 00000000..adb9ae23 --- /dev/null +++ b/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/dsl/WizardDsl.kt @@ -0,0 +1,81 @@ +package at.mocode.frontend.core.wizard.dsl + +import at.mocode.frontend.core.wizard.runtime.* + +class FlowBuilder(private val start: S) { + internal val steps = mutableMapOf>() + + fun step(id: S, block: StepBuilder.() -> Unit) { + val sb = StepBuilder() + sb.block() + steps[id] = StepDef( + onEnter = sb.onEnter, + onLeave = sb.onLeave, + transitions = sb.transitions.toList(), + otherwise = sb.otherwise + ) + } + + fun build(): WizardRuntime = SimpleWizardRuntime(start, steps.toMap()) +} + +class StepBuilder { + internal var onEnter: (suspend (WizardContext, WizardState) -> Unit)? = null + internal var onLeave: (suspend (WizardContext, WizardState) -> Unit)? = null + internal val transitions = mutableListOf>() + internal var otherwise: S? = null + + fun onEnter(block: suspend (WizardContext, WizardState) -> Unit) { + onEnter = block + } + fun onLeave(block: suspend (WizardContext, WizardState) -> Unit) { + onLeave = block + } + + fun whenGuard(id: String, g: Guard, go: S) { + transitions += Transition(id = id, target = go, guard = g) + } + + fun otherwise(go: S) { + otherwise = go + } +} + +internal data class StepDef( + val onEnter: (suspend (WizardContext, WizardState) -> Unit)? = null, + val onLeave: (suspend (WizardContext, WizardState) -> Unit)? = null, + val transitions: List> = emptyList(), + val otherwise: S? = null +) + +private class SimpleWizardRuntime( + override val start: S, + private val steps: Map> +) : WizardRuntime { + + override fun next(ctx: WizardContext, state: WizardState): WizardState { + val def = steps[state.current] + if (def == null) return state // unknown step, no-op + + val target = def.transitions.firstOrNull { tr -> + @Suppress("UNCHECKED_CAST") + (tr.guard as? Guard)?.invoke(ctx, state.acc) ?: false + }?.target ?: def.otherwise ?: state.current + + if (target == state.current) return state + return state.copy(current = target, history = state.history + state.current) + } + + override fun back(state: WizardState): WizardState { + val history = state.history + if (history.isEmpty()) return state + val prev = history.last() + return state.copy(current = prev, history = history.dropLast(1)) + } +} + +fun flow(start: S, build: FlowBuilder.() -> Unit): WizardRuntime { + val fb = FlowBuilder(start) + fb.apply(build) + return fb.build() +} diff --git a/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/runtime/WizardCore.kt b/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/runtime/WizardCore.kt new file mode 100644 index 00000000..5ea33da8 --- /dev/null +++ b/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/runtime/WizardCore.kt @@ -0,0 +1,41 @@ +package at.mocode.frontend.core.wizard.runtime + +import at.mocode.frontend.core.domain.repository.MasterdataStats +import at.mocode.frontend.core.navigation.AppScreen + +// Base marker for step IDs (sealed interface implementations per flow) +interface StepId + +data class WizardContext( + val origin: AppScreen, + val role: String? = null, + val isOnline: Boolean = true, + val stats: MasterdataStats? = null +) + +data class WizardState( + val current: S, + val history: List = emptyList(), + val acc: A, + val errors: List = emptyList() +) + +typealias Guard = (WizardContext, A) -> Boolean + +data class Transition( + val id: String, + val target: S, + val guard: Guard? = null +) + +interface StepEffects { + suspend fun onEnter(ctx: WizardContext, state: WizardState) {} + suspend fun onLeave(ctx: WizardContext, state: WizardState) {} + suspend fun onComplete(ctx: WizardContext, state: WizardState) {} +} + +interface WizardRuntime { + val start: S + fun next(ctx: WizardContext, state: WizardState): WizardState + fun back(state: WizardState): WizardState +} diff --git a/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/samples/EventFlowSample.kt b/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/samples/EventFlowSample.kt new file mode 100644 index 00000000..5a280890 --- /dev/null +++ b/frontend/core/wizard/src/commonMain/kotlin/at/mocode/frontend/core/wizard/samples/EventFlowSample.kt @@ -0,0 +1,30 @@ +package at.mocode.frontend.core.wizard.samples + +import at.mocode.frontend.core.navigation.AppScreen +import at.mocode.frontend.core.wizard.dsl.flow +import at.mocode.frontend.core.wizard.runtime.Guard +import at.mocode.frontend.core.wizard.runtime.StepId +import at.mocode.frontend.core.wizard.runtime.WizardState + +// Minimaler, selbstenthaltener Demo-Flow (2 Steps) für den Spike +sealed interface DemoEventStep : StepId { + data object ZnsCheck : DemoEventStep + data object VeranstalterSelection : DemoEventStep +} + +data class DemoEventAcc(val dummy: String = "") + +object DemoEventGuards { + val hasZns: Guard = { ctx, _ -> (ctx.stats?.vereinCount ?: 0) > 0 } +} + +val DemoEventFlow = flow(start = DemoEventStep.ZnsCheck) { + step(DemoEventStep.ZnsCheck) { + whenGuard("hasZns", DemoEventGuards.hasZns, go = DemoEventStep.VeranstalterSelection) + otherwise(DemoEventStep.VeranstalterSelection) + } +} + +// Hilfsfunktion für einfache manuelle Nutzung im Spike +fun demoStartState(origin: AppScreen, acc: DemoEventAcc = DemoEventAcc()): WizardState = + WizardState(current = DemoEventStep.ZnsCheck, acc = acc)