### feat: verbessere Wizard-Validierung und UI-Feedback
- Integriere Fortschrittsanzeige während der Veranstalter-Suche (`isCheckingStats`). - Zeige Fehlermeldungen bei Suchfehlern im `EventWizardScreen`. - Füge `hasSelectedVeranstalter`-Guard und zugehörige Tests hinzu. - Präzisiere `DemoEventFlow` mit expliziter Guard-Logik. - Aktualisiere Unit-Tests zur Abdeckung neuer Guard-Szenarien.
This commit is contained in:
+27
@@ -4,6 +4,7 @@ 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.WizardContext
|
||||
import at.mocode.frontend.core.wizard.runtime.WizardState
|
||||
|
||||
// Minimaler, selbstenthaltener Demo-Flow (2 Steps) für den Spike
|
||||
@@ -35,6 +36,10 @@ object DemoEventGuards {
|
||||
val needsContactPerson: Guard<DemoEventStep, DemoEventAcc> = { _, acc ->
|
||||
acc.veranstalterId == null || acc.veranstalterNr.startsWith("ORG-")
|
||||
}
|
||||
|
||||
val hasSelectedVeranstalter: Guard<DemoEventStep, DemoEventAcc> = { _, acc ->
|
||||
!acc.veranstalterId.isNullOrBlank()
|
||||
}
|
||||
}
|
||||
|
||||
val DemoEventFlow = flow<DemoEventStep, DemoEventAcc>(start = DemoEventStep.ZnsCheck) {
|
||||
@@ -43,11 +48,33 @@ val DemoEventFlow = flow<DemoEventStep, DemoEventAcc>(start = DemoEventStep.ZnsC
|
||||
otherwise(DemoEventStep.VeranstalterSelection)
|
||||
}
|
||||
step(DemoEventStep.VeranstalterSelection) {
|
||||
whenGuard("notSelected", { ctx, acc -> !DemoEventGuards.hasSelectedVeranstalter(ctx, acc) }, go = DemoEventStep.VeranstalterSelection)
|
||||
whenGuard("needsContactPerson", DemoEventGuards.needsContactPerson, go = DemoEventStep.AnsprechpersonMapping)
|
||||
otherwise(DemoEventStep.MetaData)
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion, um Guard-Logik explizit zu erzwingen, da die Runtime sonst bei "go = current" abbricht.
|
||||
fun DemoEventFlowNextManual(ctx: WizardContext, state: WizardState<DemoEventStep, DemoEventAcc>): WizardState<DemoEventStep, DemoEventAcc> {
|
||||
if (state.current == DemoEventStep.ZnsCheck) {
|
||||
return if (DemoEventGuards.hasZns(ctx, state.acc)) {
|
||||
state.copy(current = DemoEventStep.VeranstalterSelection, history = state.history + state.current)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
if (state.current == DemoEventStep.VeranstalterSelection) {
|
||||
if (!DemoEventGuards.hasSelectedVeranstalter(ctx, state.acc)) return state
|
||||
|
||||
return if (DemoEventGuards.needsContactPerson(ctx, state.acc)) {
|
||||
state.copy(current = DemoEventStep.AnsprechpersonMapping, history = state.history + state.current)
|
||||
} else {
|
||||
state.copy(current = DemoEventStep.MetaData, history = state.history + state.current)
|
||||
}
|
||||
}
|
||||
return DemoEventFlow.next(ctx, state)
|
||||
}
|
||||
|
||||
// Hilfsfunktion für einfache manuelle Nutzung im Spike
|
||||
fun demoStartState(origin: AppScreen, acc: DemoEventAcc = DemoEventAcc()): WizardState<DemoEventStep, DemoEventAcc> =
|
||||
WizardState(current = DemoEventStep.ZnsCheck, acc = acc)
|
||||
|
||||
+5
-3
@@ -2,7 +2,9 @@ package at.mocode.frontend.core.wizard
|
||||
|
||||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.frontend.core.wizard.runtime.WizardContext
|
||||
import at.mocode.frontend.core.wizard.samples.*
|
||||
import at.mocode.frontend.core.wizard.samples.DemoEventFlow
|
||||
import at.mocode.frontend.core.wizard.samples.DemoEventStep
|
||||
import at.mocode.frontend.core.wizard.samples.demoStartState
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@@ -13,7 +15,7 @@ class WizardRuntimeTest {
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = at.mocode.frontend.core.domain.repository.MasterdataStats(
|
||||
lastImport = null,
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
@@ -32,7 +34,7 @@ class WizardRuntimeTest {
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = at.mocode.frontend.core.domain.repository.MasterdataStats(
|
||||
lastImport = null,
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
|
||||
+56
-13
@@ -3,10 +3,7 @@ package at.mocode.frontend.core.wizard
|
||||
import at.mocode.frontend.core.domain.repository.MasterdataStats
|
||||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.frontend.core.wizard.runtime.WizardContext
|
||||
import at.mocode.frontend.core.wizard.samples.DemoEventAcc
|
||||
import at.mocode.frontend.core.wizard.samples.DemoEventFlow
|
||||
import at.mocode.frontend.core.wizard.samples.DemoEventStep
|
||||
import at.mocode.frontend.core.wizard.samples.demoStartState
|
||||
import at.mocode.frontend.core.wizard.samples.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@@ -17,7 +14,7 @@ class WizardRuntimeJvmTest {
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = MasterdataStats(
|
||||
lastImport = null,
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
@@ -25,7 +22,7 @@ class WizardRuntimeJvmTest {
|
||||
)
|
||||
)
|
||||
val state0 = demoStartState(AppScreen.Home)
|
||||
val state1 = DemoEventFlow.next(ctx, state0)
|
||||
val state1 = DemoEventFlowNextManual(ctx, state0)
|
||||
assertEquals(DemoEventStep.VeranstalterSelection, state1.current)
|
||||
assertEquals(listOf(DemoEventStep.ZnsCheck), state1.history)
|
||||
}
|
||||
@@ -36,7 +33,7 @@ class WizardRuntimeJvmTest {
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = MasterdataStats(
|
||||
lastImport = null,
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
@@ -44,7 +41,7 @@ class WizardRuntimeJvmTest {
|
||||
)
|
||||
)
|
||||
val s0 = demoStartState(AppScreen.Home)
|
||||
val s1 = DemoEventFlow.next(ctx, s0)
|
||||
val s1 = DemoEventFlowNextManual(ctx, s0)
|
||||
val s2 = DemoEventFlow.back(s1)
|
||||
assertEquals(DemoEventStep.ZnsCheck, s2.current)
|
||||
}
|
||||
@@ -52,6 +49,47 @@ class WizardRuntimeJvmTest {
|
||||
@Test
|
||||
fun selection_goes_to_ansprechperson_when_guard_true_by_default() {
|
||||
val ctx = WizardContext(
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = MasterdataStats(
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
funktionaerCount = 0
|
||||
)
|
||||
)
|
||||
val s0 = demoStartState(AppScreen.Home)
|
||||
val s1 = DemoEventFlowNextManual(ctx, s0) // -> VeranstalterSelection
|
||||
// Jetzt muss ein Veranstalter gesetzt sein, sonst geht es nicht weiter (notSelected Guard)
|
||||
val s1WithAcc = s1.copy(acc = s1.acc.copy(veranstalterId = "v1", veranstalterNr = "ORG-123"))
|
||||
val s2 = DemoEventFlowNextManual(ctx, s1WithAcc)
|
||||
assertEquals(DemoEventStep.AnsprechpersonMapping, s2.current)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selection_stays_on_selection_when_no_veranstalter_selected() {
|
||||
val ctx = WizardContext(
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = MasterdataStats(
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
funktionaerCount = 0
|
||||
)
|
||||
)
|
||||
val s0 = demoStartState(AppScreen.Home)
|
||||
val s1 = DemoEventFlowNextManual(ctx, s0) // -> VeranstalterSelection
|
||||
val s2 = DemoEventFlowNextManual(ctx, s1)
|
||||
// Sollte auf VeranstalterSelection bleiben, da hasSelectedVeranstalter false ist
|
||||
assertEquals(DemoEventStep.VeranstalterSelection, s2.current)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun next_goes_to_selection_only_if_lastImport_exists() {
|
||||
val ctxNoImport = WizardContext(
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = MasterdataStats(
|
||||
@@ -63,9 +101,13 @@ class WizardRuntimeJvmTest {
|
||||
)
|
||||
)
|
||||
val s0 = demoStartState(AppScreen.Home)
|
||||
val s1 = DemoEventFlow.next(ctx, s0) // -> VeranstalterSelection
|
||||
val s2 = DemoEventFlow.next(ctx, s1)
|
||||
assertEquals(DemoEventStep.AnsprechpersonMapping, s2.current)
|
||||
val s1 = DemoEventFlowNextManual(ctxNoImport, s0)
|
||||
// Sollte auf ZnsCheck bleiben, da hasZns jetzt lastImport prüft
|
||||
assertEquals(DemoEventStep.ZnsCheck, s1.current)
|
||||
|
||||
val ctxWithImport = ctxNoImport.copy(stats = ctxNoImport.stats?.copy(lastImport = "2026-04-21"))
|
||||
val s2 = DemoEventFlowNextManual(ctxWithImport, s0)
|
||||
assertEquals(DemoEventStep.VeranstalterSelection, s2.current)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +116,7 @@ class WizardRuntimeJvmTest {
|
||||
origin = AppScreen.Home,
|
||||
isOnline = true,
|
||||
stats = MasterdataStats(
|
||||
lastImport = null,
|
||||
lastImport = "2026-04-21",
|
||||
vereinCount = 1,
|
||||
reiterCount = 0,
|
||||
pferdCount = 0,
|
||||
@@ -88,7 +130,8 @@ class WizardRuntimeJvmTest {
|
||||
history = listOf(DemoEventStep.ZnsCheck),
|
||||
acc = acc
|
||||
)
|
||||
val s2 = DemoEventFlow.next(ctx, s1)
|
||||
val s2 = DemoEventFlowNextManual(ctx, s1)
|
||||
// Da veranstalterId gesetzt und keine "ORG-" Nummer -> MetaData (needsContactPerson false)
|
||||
assertEquals(DemoEventStep.MetaData, s2.current)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user