chore: refaktoriere Veranstaltungs-UI zu Events, implementiere ZNS-Suche und verbessere Navigationslogik

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-21 13:41:06 +02:00
parent 9b4af2bb56
commit 574f8c470c
18 changed files with 673 additions and 174 deletions
@@ -1,6 +1,7 @@
package at.mocode.frontend.features.profile.di
import at.mocode.frontend.features.profile.data.ProfileApiClient
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -8,4 +9,5 @@ import org.koin.dsl.module
val profileModule = module {
single { ProfileApiClient(get(named("apiClient")), get()) }
single { ProfileViewModel(get()) }
factory { ProfileOnboardingViewModel(get(), get()) }
}
@@ -0,0 +1,169 @@
package at.mocode.frontend.features.profile.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
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
import at.mocode.frontend.core.designsystem.components.MsTextField
@Composable
fun ProfileOnboardingScreen(
viewModel: ProfileOnboardingViewModel,
onFinish: () -> Unit
) {
val state = viewModel.state
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = "Willkommen bei der Meldestelle",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
LinearProgressIndicator(
progress = {
when (state.currentStep) {
OnboardingStep.SEARCH_ZNS -> 0.33f
OnboardingStep.CONFIRM_DATA -> 0.66f
OnboardingStep.FINISHED -> 1f
}
},
modifier = Modifier.fillMaxWidth()
)
Box(modifier = Modifier.weight(1f)) {
when (state.currentStep) {
OnboardingStep.SEARCH_ZNS -> SearchStep(viewModel)
OnboardingStep.CONFIRM_DATA -> ConfirmStep(viewModel)
OnboardingStep.FINISHED -> FinishedStep(state, onFinish)
}
}
if (state.currentStep != OnboardingStep.FINISHED) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(onClick = { viewModel.back() }, enabled = state.currentStep != OnboardingStep.SEARCH_ZNS) {
Text("Zurück")
}
if (state.currentStep == OnboardingStep.CONFIRM_DATA) {
Button(onClick = { viewModel.confirmAndLink() }, enabled = !state.isLoading) {
if (state.isLoading) CircularProgressIndicator(Modifier.size(16.dp))
else Text("Daten bestätigen & Verknüpfen")
}
}
}
}
}
}
@Composable
private fun SearchStep(viewModel: ProfileOnboardingViewModel) {
val state = viewModel.state
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Wer bist du?", style = MaterialTheme.typography.titleLarge)
Text("Suchen Sie nach Ihrer Satznummer oder Ihrem Namen in den ZNS-Stammdaten.")
MsTextField(
value = state.searchQuery,
onValueChange = { viewModel.onSearchQueryChange(it) },
label = "Suche (Name oder Satznummer)",
placeholder = "z.B. Stroblmair",
modifier = Modifier.fillMaxWidth(),
leadingIcon = Icons.Default.Search
)
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(state.searchResults) { reiter ->
Card(
onClick = { viewModel.selectReiter(reiter) },
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(Icons.Default.Person, null)
Column {
Text("${reiter.vorname} ${reiter.nachname}", fontWeight = FontWeight.Bold)
Text("Satznr: ${reiter.satznummer ?: "N/A"} | Lizenz: ${reiter.lizenz ?: "Keine"}")
}
}
}
}
}
}
}
@Composable
private fun ConfirmStep(viewModel: ProfileOnboardingViewModel) {
val state = viewModel.state
val reiter = state.selectedReiter ?: return
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Daten bestätigen", style = MaterialTheme.typography.titleLarge)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Vorname: ${reiter.vorname}")
Text("Nachname: ${reiter.nachname}")
Text("Satznummer: ${reiter.satznummer ?: "N/A"}")
Text("Lizenz: ${reiter.lizenz ?: "Keine"}")
Text("Klasse: ${reiter.lizenzKlasse}")
}
}
Text(
"Durch das Verknüpfen werden Ihre Aktionen in der App mit Ihrer offiziellen ZNS-Identität hinterlegt.",
style = MaterialTheme.typography.bodyMedium
)
if (state.error != null) {
Text(state.error, color = MaterialTheme.colorScheme.error)
}
}
}
@Composable
private fun FinishedStep(state: ProfileOnboardingState, onFinish: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.height(16.dp))
Text("Profil erfolgreich verknüpft!", style = MaterialTheme.typography.headlineSmall)
Text("Willkommen, ${state.selectedReiter?.vorname ?: ""} ${state.selectedReiter?.nachname ?: ""}!")
Spacer(Modifier.height(32.dp))
Button(onClick = onFinish) {
Text("Los geht's")
}
}
}
@@ -0,0 +1,98 @@
package at.mocode.frontend.features.profile.presentation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
import at.mocode.frontend.features.profile.data.ProfileApiClient
import at.mocode.frontend.features.profile.data.ProfileDto
import kotlinx.coroutines.launch
enum class OnboardingStep {
SEARCH_ZNS,
CONFIRM_DATA,
FINISHED
}
data class ProfileOnboardingState(
val currentStep: OnboardingStep = OnboardingStep.SEARCH_ZNS,
val searchQuery: String = "",
val searchResults: List<ZnsRemoteReiter> = emptyList(),
val selectedReiter: ZnsRemoteReiter? = null,
val isLoading: Boolean = false,
val error: String? = null,
val profile: ProfileDto? = null
)
class ProfileOnboardingViewModel(
private val znsImportProvider: ZnsImportProvider,
private val profileApiClient: ProfileApiClient
) : ViewModel() {
var state by mutableStateOf(ProfileOnboardingState())
private set
fun onSearchQueryChange(query: String) {
state = state.copy(searchQuery = query)
if (query.length >= 3) {
search()
}
}
private fun search() {
viewModelScope.launch {
state = state.copy(isLoading = true, error = null)
try {
znsImportProvider.searchRemote(state.searchQuery)
state = state.copy(
isLoading = false,
searchResults = znsImportProvider.state.remoteReiter
)
} catch (e: Exception) {
state = state.copy(isLoading = false, error = "Fehler bei der ZNS-Suche: ${e.message}")
}
}
}
fun selectReiter(reiter: ZnsRemoteReiter) {
state = state.copy(
selectedReiter = reiter,
currentStep = OnboardingStep.CONFIRM_DATA
)
}
fun confirmAndLink() {
val reiter = state.selectedReiter ?: return
viewModelScope.launch {
state = state.copy(isLoading = true, error = null)
try {
val satznr = reiter.satznummer ?: ""
val profile = profileApiClient.linkToZns(satznr)
if (profile != null) {
state = state.copy(
isLoading = false,
profile = profile,
currentStep = OnboardingStep.FINISHED
)
} else {
state = state.copy(isLoading = false, error = "Verknüpfung fehlgeschlagen.")
}
} catch (e: Exception) {
state = state.copy(isLoading = false, error = "Fehler beim Verknüpfen: ${e.message}")
}
}
}
fun back() {
state = state.copy(
currentStep = when (state.currentStep) {
OnboardingStep.SEARCH_ZNS -> OnboardingStep.SEARCH_ZNS
OnboardingStep.CONFIRM_DATA -> OnboardingStep.SEARCH_ZNS
OnboardingStep.FINISHED -> OnboardingStep.CONFIRM_DATA
}
)
}
}