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:
+2
-2
@@ -3,8 +3,8 @@ package at.mocode.frontend.core.auth.di
|
||||
import at.mocode.frontend.core.auth.data.AuthApiClient
|
||||
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
||||
import at.mocode.frontend.core.network.TokenProvider
|
||||
import at.mocode.frontend.core.domain.AppConstants
|
||||
import at.mocode.frontend.core.network.TokenProvider
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
@@ -24,7 +24,7 @@ val authModule = module {
|
||||
}
|
||||
|
||||
// LoginViewModel
|
||||
factory { LoginViewModel(get(), get(), get(named("apiClient"))) }
|
||||
factory { LoginViewModel(get(), get()) }
|
||||
|
||||
// Brücke zum TokenProvider des Kernnetzwerks, ohne dort eine harte Abhängigkeit hinzuzufügen
|
||||
single<TokenProvider> {
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package at.mocode.frontend.core.auth.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Login
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsCard
|
||||
|
||||
/**
|
||||
* Eine Plug-and-Play Komponente zur Anzeige des aktuellen Authentifizierungs-Status.
|
||||
* Kann überall (Sidebar, Header, Screens) eingesetzt werden.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthStatusCard(
|
||||
viewModel: LoginViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onLoginClick: () -> Unit = {}
|
||||
) {
|
||||
val authState by viewModel.authState.collectAsState()
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
MsCard(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AccountCircle,
|
||||
contentDescription = null,
|
||||
tint = if (authState.isAuthenticated) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = if (authState.isAuthenticated) "Angemeldet als" else "Nicht angemeldet",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = if (authState.isAuthenticated) (authState.username ?: "Unbekannt") else "Gast",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (authState.isAuthenticated) {
|
||||
Button(
|
||||
onClick = { viewModel.logout() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Abmelden")
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = onLoginClick,
|
||||
enabled = !uiState.isOidcLoading
|
||||
) {
|
||||
if (uiState.isOidcLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.AutoMirrored.Filled.Login, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Anmelden")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authState.isAuthenticated && authState.roles.isNotEmpty()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
authState.roles.forEach { role ->
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = { Text(role, style = MaterialTheme.typography.labelSmall) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.core.auth.data.*
|
||||
import at.mocode.frontend.core.domain.AppConstants
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -34,13 +33,15 @@ data class LoginUiState(
|
||||
*/
|
||||
class LoginViewModel(
|
||||
private val authTokenManager: AuthTokenManager,
|
||||
private val authApiClient: AuthApiClient,
|
||||
private val apiClient: HttpClient
|
||||
private val authApiClient: AuthApiClient
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(LoginUiState())
|
||||
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _authState = MutableStateFlow(AuthState())
|
||||
val authState: StateFlow<AuthState> = _authState.asStateFlow()
|
||||
|
||||
// PKCE-State für den laufenden OIDC-Flow (in-memory)
|
||||
private var pendingCodeVerifier: String? = null
|
||||
private var pendingState: String? = null
|
||||
@@ -48,9 +49,10 @@ class LoginViewModel(
|
||||
init {
|
||||
// AuthTokenManager-State beobachten → UI synchron halten
|
||||
viewModelScope.launch {
|
||||
authTokenManager.authState.collect { authState ->
|
||||
_uiState.value = _uiState.value.copy(isAuthenticated = authState.isAuthenticated)
|
||||
if (!authState.isAuthenticated) {
|
||||
authTokenManager.authState.collect { auth ->
|
||||
_authState.value = auth
|
||||
_uiState.value = _uiState.value.copy(isAuthenticated = auth.isAuthenticated)
|
||||
if (!auth.isAuthenticated) {
|
||||
_uiState.value = LoginUiState()
|
||||
}
|
||||
}
|
||||
@@ -223,4 +225,11 @@ class LoginViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Abmelden. */
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
authTokenManager.clearToken()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,9 +1,9 @@
|
||||
package at.mocode.frontend.core.navigation
|
||||
|
||||
sealed class AppScreen(val route: String) {
|
||||
// Onboarding (Desktop: Gerätename/Schlüssel/ZNS)
|
||||
data object Onboarding : AppScreen("/onboarding")
|
||||
data object Landing : AppScreen(Routes.HOME)
|
||||
// DeviceInitialization (Desktop: Gerätename/Schlüssel/ZNS)
|
||||
data object DeviceInitialization : AppScreen("/onboarding")
|
||||
data object PortalDashboard : AppScreen(Routes.HOME)
|
||||
data object Home : AppScreen("/home")
|
||||
data object Dashboard : AppScreen("/dashboard")
|
||||
data object CreateTournament : AppScreen("/tournament/create") // Neuer Screen
|
||||
@@ -11,11 +11,11 @@ sealed class AppScreen(val route: String) {
|
||||
// Login now accepts an optional returnTo screen to determine where to go after success
|
||||
data class Login(val returnTo: AppScreen? = null) : AppScreen(Routes.LOGIN)
|
||||
|
||||
data object Ping : AppScreen("/ping")
|
||||
data object ConnectivityCheck : AppScreen("/ping")
|
||||
data object Profile : AppScreen("/profile")
|
||||
data object OrganizerProfile : AppScreen("/organizer/profile")
|
||||
data object AuthCallback : AppScreen("/auth/callback")
|
||||
data object Nennung : AppScreen("/nennung")
|
||||
data object EntryManagement : AppScreen("/nennung")
|
||||
|
||||
// --- Desktop-Navigation (Vision_03) ---
|
||||
data object VeranstaltungVerwaltung : AppScreen("/verwaltung") // Gesamtübersicht
|
||||
@@ -38,7 +38,7 @@ sealed class AppScreen(val route: String) {
|
||||
|
||||
// data class VeranstaltungProfil(val id: Long) : AppScreen("/veranstaltung/profil/$id")
|
||||
|
||||
// Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
||||
// Neuer Flow: + neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
||||
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
||||
data object VeranstalterNeu : AppScreen("/veranstalter/neu")
|
||||
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||
@@ -46,6 +46,7 @@ sealed class AppScreen(val route: String) {
|
||||
// Neue Veranstaltungs-Konfig-Seite (aus Veranstalter-Detail oder direkt aus Cockpit)
|
||||
data class VeranstaltungKonfig(val veranstalterId: Long = 0) :
|
||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/neu")
|
||||
|
||||
data class VeranstaltungProfil(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||
|
||||
@@ -61,7 +62,6 @@ sealed class AppScreen(val route: String) {
|
||||
data object Reiter : AppScreen("/reiter")
|
||||
data object Pferde : AppScreen("/pferde")
|
||||
data object Vereine : AppScreen("/vereine")
|
||||
data object Funktionaere : AppScreen("/funktionaere")
|
||||
data object Meisterschaften : AppScreen("/meisterschaften")
|
||||
data object Cups : AppScreen("/cups")
|
||||
data object StammdatenImport : AppScreen("/stammdaten/import")
|
||||
@@ -85,17 +85,17 @@ sealed class AppScreen(val route: String) {
|
||||
|
||||
fun fromRoute(route: String): AppScreen {
|
||||
return when (route) {
|
||||
"/onboarding" -> Onboarding
|
||||
Routes.HOME -> Landing
|
||||
"/onboarding" -> DeviceInitialization
|
||||
Routes.HOME -> PortalDashboard
|
||||
"/home" -> Home
|
||||
"/dashboard" -> Dashboard
|
||||
"/tournament/create" -> CreateTournament
|
||||
Routes.LOGIN, Routes.Auth.LOGIN -> Login()
|
||||
"/ping" -> Ping
|
||||
"/ping" -> ConnectivityCheck
|
||||
"/profile" -> Profile
|
||||
"/organizer/profile" -> OrganizerProfile
|
||||
"/auth/callback" -> AuthCallback
|
||||
"/nennung" -> Nennung
|
||||
"/nennung" -> EntryManagement
|
||||
"/verwaltung" -> VeranstaltungVerwaltung
|
||||
"/pferde/verwaltung" -> PferdVerwaltung
|
||||
"/reiter/verwaltung" -> ReiterVerwaltung
|
||||
@@ -139,7 +139,7 @@ sealed class AppScreen(val route: String) {
|
||||
VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
|
||||
return VeranstaltungProfil(verId.toLong(), vId.toLong())
|
||||
}
|
||||
Landing // Default fallback
|
||||
PortalDashboard // Default fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -10,13 +10,15 @@ import kotlin.test.assertTrue
|
||||
|
||||
private class FakeNav : NavigationPort {
|
||||
var last: String? = null
|
||||
override val currentScreen: StateFlow<AppScreen> = MutableStateFlow(AppScreen.Landing)
|
||||
override val currentScreen: StateFlow<AppScreen> = MutableStateFlow(AppScreen.PortalDashboard)
|
||||
override fun navigateTo(route: String) {
|
||||
last = route
|
||||
}
|
||||
|
||||
override fun navigateToScreen(screen: AppScreen) {
|
||||
last = screen.route
|
||||
}
|
||||
|
||||
override fun navigateBack() {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user