refactor(desktop, core): Onboarding zu DeviceInitialization umbenannt, Navigation und Screens angepasst
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"geraetName": "Meldestelle",
|
||||
"sharedKey": "Meldestelle",
|
||||
"backupPath": "/mocode/Meldestelle/docs/temp",
|
||||
"backupPath": "/home/stefan/WsMeldestelle/Meldestelle/meldestelle/docs/temp",
|
||||
"networkRole": "MASTER",
|
||||
"expectedClients": [
|
||||
{
|
||||
"name": "Zeithnehmer",
|
||||
"role": "ZEITNEHMER"
|
||||
"name": "Richter-Turm",
|
||||
"role": "RICHTER"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+9
-8
@@ -35,18 +35,18 @@ fun DesktopApp() {
|
||||
val currentScreen by nav.currentScreen.collectAsState()
|
||||
val loginViewModel: LoginViewModel = koinViewModel()
|
||||
|
||||
// Onboarding-Check beim Start
|
||||
// DeviceInitialization-Check beim Start
|
||||
LaunchedEffect(Unit) {
|
||||
if (!SettingsManager.isConfigured()) {
|
||||
nav.navigateToScreen(AppScreen.Onboarding)
|
||||
nav.navigateToScreen(AppScreen.DeviceInitialization)
|
||||
}
|
||||
}
|
||||
|
||||
val authState by authTokenManager.authState.collectAsState()
|
||||
|
||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
|
||||
// Vision_03 Update: Wir starten mit Onboarding
|
||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Onboarding
|
||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt
|
||||
// Vision_03 Update: Wir starten mit DeviceInitialization
|
||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.DeviceInitialization
|
||||
&& currentScreen !is AppScreen.VeranstaltungVerwaltung
|
||||
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
|
||||
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
|
||||
@@ -57,10 +57,11 @@ fun DesktopApp() {
|
||||
&& currentScreen !is AppScreen.VereinVerwaltung
|
||||
&& currentScreen !is AppScreen.StammdatenImport
|
||||
&& currentScreen !is AppScreen.NennungsEingang
|
||||
&& currentScreen !is AppScreen.ConnectivityCheck
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Standard: Start im Onboarding
|
||||
nav.navigateToScreen(AppScreen.Onboarding)
|
||||
// Standard: Start im DeviceInitialization
|
||||
nav.navigateToScreen(AppScreen.DeviceInitialization)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ fun DesktopApp() {
|
||||
val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
|
||||
nav.navigateToScreen(returnTo)
|
||||
},
|
||||
onBack = { /* Desktop hat keine Landing-Page */ },
|
||||
onBack = { /* Desktop hat keine PortalDashboard-Page */ },
|
||||
)
|
||||
|
||||
else -> {
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
* Hält den aktuellen Screen als StateFlow, den DesktopApp beobachtet.
|
||||
*/
|
||||
class DesktopNavigationPort : NavigationPort {
|
||||
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.Onboarding)
|
||||
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.DeviceInitialization)
|
||||
override val currentScreen: StateFlow<AppScreen> = _currentScreen.asStateFlow()
|
||||
|
||||
// Backstack zur Speicherung des Verlaufs
|
||||
@@ -29,7 +29,7 @@ class DesktopNavigationPort : NavigationPort {
|
||||
val current = _currentScreen.value
|
||||
if (current != screen) {
|
||||
backStack.add(current)
|
||||
// Begrenzung des Backstacks auf z.B. 50 Einträge
|
||||
// Begrenzung des Backstacks auf z. B. 50 Einträge
|
||||
if (backStack.size > 50) backStack.removeAt(0)
|
||||
}
|
||||
_currentScreen.value = screen
|
||||
@@ -41,8 +41,8 @@ class DesktopNavigationPort : NavigationPort {
|
||||
println("[DesktopNav] navigateBack -> $previousScreen")
|
||||
_currentScreen.value = previousScreen
|
||||
} else {
|
||||
println("[DesktopNav] navigateBack -> Stack leer, bleibe bei Onboarding")
|
||||
_currentScreen.value = AppScreen.Onboarding
|
||||
println("[DesktopNav] navigateBack -> Stack leer, bleibe bei DeviceInitialization")
|
||||
_currentScreen.value = AppScreen.DeviceInitialization
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-46
@@ -60,7 +60,6 @@ import org.koin.compose.viewmodel.koinViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||
private val TopBarColor = Color(0xFF1E3A8A)
|
||||
private val TopBarTextColor = Color.White
|
||||
|
||||
/**
|
||||
@@ -79,14 +78,14 @@ fun DesktopMainLayout(
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
println("[Navigation] Rendering Screen: ${currentScreen::class.simpleName} (Details: $currentScreen)")
|
||||
// Onboarding-Daten (On-the-fly geladen oder Default)
|
||||
// DeviceInitialization-Daten (On-the-fly geladen oder Default)
|
||||
var onboardingSettings by remember { mutableStateOf(SettingsManager.loadSettings() ?: OnboardingSettings()) }
|
||||
|
||||
// Automatische Umleitung zum Onboarding, wenn Setup fehlt (außer wir sind bereits dort)
|
||||
// Automatische Umleitung zum DeviceInitialization, wenn Setup fehlt (außer wir sind bereits dort)
|
||||
LaunchedEffect(onboardingSettings) {
|
||||
if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.Onboarding) {
|
||||
println("[DesktopNav] Setup fehlt -> Umleitung zum Onboarding")
|
||||
onNavigate(AppScreen.Onboarding)
|
||||
if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.DeviceInitialization) {
|
||||
println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization")
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@ fun DesktopMainLayout(
|
||||
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
DesktopFooterBar(
|
||||
settings = onboardingSettings,
|
||||
onSetupClick = { onNavigate(AppScreen.Onboarding) }
|
||||
onSetupClick = { onNavigate(AppScreen.DeviceInitialization) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -182,9 +181,9 @@ private fun DesktopNavRail(
|
||||
|
||||
NavRailItem(
|
||||
icon = Icons.Default.WifiTethering,
|
||||
label = "Sync",
|
||||
selected = currentScreen is AppScreen.Ping,
|
||||
onClick = { onNavigate(AppScreen.Ping) }
|
||||
label = "ConnectivityCheck",
|
||||
selected = currentScreen is AppScreen.ConnectivityCheck,
|
||||
onClick = { onNavigate(AppScreen.ConnectivityCheck) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
@@ -192,8 +191,8 @@ private fun DesktopNavRail(
|
||||
NavRailItem(
|
||||
icon = Icons.Default.AppRegistration,
|
||||
label = "Setup",
|
||||
selected = currentScreen is AppScreen.Onboarding,
|
||||
onClick = { onNavigate(AppScreen.Onboarding) }
|
||||
selected = currentScreen is AppScreen.DeviceInitialization,
|
||||
onClick = { onNavigate(AppScreen.DeviceInitialization) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -264,7 +263,7 @@ private fun DesktopTopHeader(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (currentScreen !is AppScreen.Onboarding) {
|
||||
if (currentScreen !is AppScreen.DeviceInitialization) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
@@ -439,10 +438,10 @@ private fun BreadcrumbContent(
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Ping -> {
|
||||
is AppScreen.ConnectivityCheck -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Ping Service",
|
||||
text = "Konnektivitäts-Diagnose",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
@@ -511,27 +510,6 @@ private fun InvalidContextNotice(message: String, onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderScreen(
|
||||
title: String,
|
||||
onBack: () -> Unit,
|
||||
onAction: (() -> Unit)? = null,
|
||||
actionLabel: String = "Aktion ausführen"
|
||||
) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.headlineMedium)
|
||||
Text("Dieser Screen ist noch in Arbeit (Placeholder)", color = Color.Gray)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = onBack) { Text("Zurück") }
|
||||
if (onAction != null) {
|
||||
Button(onClick = onAction) { Text(actionLabel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen.
|
||||
*/
|
||||
@@ -544,9 +522,9 @@ private fun DesktopContentArea(
|
||||
onSettingsChange: (OnboardingSettings) -> Unit,
|
||||
) {
|
||||
when (currentScreen) {
|
||||
// Onboarding (Geräte-Setup)
|
||||
is AppScreen.Onboarding -> {
|
||||
println("[Screen] Rendering Onboarding")
|
||||
// DeviceInitialization (Geräte-Setup)
|
||||
is AppScreen.DeviceInitialization -> {
|
||||
println("[Screen] Rendering DeviceInitialization")
|
||||
OnboardingScreen(
|
||||
settings = settings,
|
||||
onSettingsChange = onSettingsChange,
|
||||
@@ -656,9 +634,8 @@ private fun DesktopContentArea(
|
||||
)
|
||||
|
||||
/*
|
||||
is AppScreen.VeranstaltungProfil -> PlaceholderScreen("Veranstaltung-Profil #${currentScreen.id}",
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungVerwaltung) }
|
||||
)
|
||||
is AppScreen.VeranstaltungProfil -> VeranstaltungProfilScreen(id = currentScreen.id,
|
||||
onBack = onBack)
|
||||
*/
|
||||
|
||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||
@@ -812,13 +789,25 @@ private fun DesktopContentArea(
|
||||
}
|
||||
}
|
||||
|
||||
// Ping-Screen
|
||||
is AppScreen.Ping -> {
|
||||
println("[Screen] Rendering Ping")
|
||||
// ConnectivityCheck-Screen
|
||||
is AppScreen.ConnectivityCheck -> {
|
||||
println("[Screen] Rendering ConnectivityCheck")
|
||||
val pingViewModel: PingViewModel = koinInject()
|
||||
PingScreen(
|
||||
viewModel = pingViewModel,
|
||||
onBack = onBack,
|
||||
onNavigateToLogin = { onNavigate(AppScreen.Login(returnTo = AppScreen.ConnectivityCheck)) }
|
||||
)
|
||||
}
|
||||
|
||||
// Login-Screen (Integration)
|
||||
is AppScreen.Login -> {
|
||||
println("[Screen] Rendering Login")
|
||||
val loginViewModel: at.mocode.frontend.core.auth.presentation.LoginViewModel = koinInject()
|
||||
at.mocode.frontend.core.auth.presentation.LoginScreen(
|
||||
viewModel = loginViewModel,
|
||||
onLoginSuccess = onBack,
|
||||
onBack = onBack
|
||||
)
|
||||
}
|
||||
|
||||
@@ -856,7 +845,7 @@ private fun DesktopContentArea(
|
||||
SeriesScreen(title = "Cups", onBack = onBack)
|
||||
}
|
||||
|
||||
is AppScreen.Nennung -> {
|
||||
is AppScreen.EntryManagement -> {
|
||||
val nennungViewModel: NennungViewModel = koinViewModel()
|
||||
NennungsMaske(
|
||||
viewModel = nennungViewModel,
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
||||
fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkProcessed: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Details zur Online-Nennung") },
|
||||
title = { Text("Details zur Online-EntryManagement") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
DetailRow("Absender", mail.sender)
|
||||
|
||||
+4
-7
@@ -1,13 +1,13 @@
|
||||
package at.mocode.desktop.screens.onboarding
|
||||
|
||||
/**
|
||||
* Validierungslogik für den Onboarding-Wizard.
|
||||
* Validierungslogik für den DeviceInitialization-Wizard.
|
||||
*
|
||||
* Extrahiert aus `OnboardingScreen` für isolierte Unit-Tests (B-2).
|
||||
* Regeln gemäß Onboarding-Spezifikation:
|
||||
* Regeln gemäß DeviceInitialization-Spezifikation:
|
||||
* - Gerätename: mindestens 3 Zeichen (nach trim)
|
||||
* - Sicherheitsschlüssel: mindestens 8 Zeichen (nach trim)
|
||||
* - Backup-Pfad: darf nicht leer sein und muss existieren (Prüfung optional hier)
|
||||
* - Backup-Pfad: Darf nicht leer sein und muss existieren (Prüfung optional hier)
|
||||
* - Sync-Intervall: zwischen 1 und 60 Minuten
|
||||
*/
|
||||
object OnboardingValidator {
|
||||
@@ -18,9 +18,6 @@ object OnboardingValidator {
|
||||
/** Mindestlänge für den Sicherheitsschlüssel. */
|
||||
const val MIN_KEY_LENGTH = 8
|
||||
|
||||
/** Standard-Sync-Intervall in Minuten. */
|
||||
const val DEFAULT_SYNC_INTERVAL = 30
|
||||
|
||||
/** Gibt `true` zurück, wenn der Gerätename gültig ist. */
|
||||
fun isNameValid(name: String): Boolean = name.trim().length >= MIN_NAME_LENGTH
|
||||
|
||||
@@ -35,7 +32,7 @@ object OnboardingValidator {
|
||||
|
||||
/**
|
||||
* Gibt `true` zurück, wenn alle Pflichtfelder gültig sind und
|
||||
* der „Weiter"-Button aktiviert werden darf.
|
||||
* der „Weiter“-Button aktiviert werden darf.
|
||||
*/
|
||||
fun canContinue(settings: OnboardingSettings): Boolean {
|
||||
val basicValid = isNameValid(settings.geraetName) &&
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* B-2 Test-Suite: Onboarding-Wizard Edge-Cases
|
||||
* B-2 Test-Suite: DeviceInitialization-Wizard Edge-Cases
|
||||
*
|
||||
* Testet die Validierungslogik des Onboarding-Wizards isoliert via [OnboardingValidator].
|
||||
* Testet die Validierungslogik des DeviceInitialization-Wizards isoliert via [OnboardingValidator].
|
||||
* Die `rememberSaveable`-Regression (Zurück-Navigation behält Felder) ist durch den
|
||||
* Fix in OnboardingScreen.kt (remember → rememberSaveable) abgesichert; ein
|
||||
* Compose-UI-Test dafür ist auf JVM-Desktop ohne Instrumentation nicht möglich.
|
||||
@@ -144,7 +144,7 @@ class OnboardingValidatorTest {
|
||||
assertTrue(second)
|
||||
}
|
||||
|
||||
// ─── rememberSaveable Regressions-Dokumentation ─────────────────────────────
|
||||
// ─── rememberSavable Regressions-Dokumentation ─────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `B2 Regression rememberSaveable - Validator akzeptiert vorausgefüllte Werte nach Ruecknavigation`() {
|
||||
|
||||
Reference in New Issue
Block a user