(vision) SCS/DDD
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
js(IR) {
|
||||
browser {
|
||||
commonWebpackConfig {
|
||||
outputFileName = "composeApp.js"
|
||||
}
|
||||
}
|
||||
binaries.executable()
|
||||
}
|
||||
|
||||
jvm("desktop")
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
|
||||
// Project dependencies
|
||||
implementation(project(":shared-kernel"))
|
||||
implementation(project(":member-management"))
|
||||
implementation(project(":master-data"))
|
||||
implementation(project(":horse-registry"))
|
||||
implementation(project(":event-management"))
|
||||
|
||||
// Kotlinx dependencies
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("com.benasher44:uuid:0.8.4")
|
||||
|
||||
// Navigation
|
||||
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha07")
|
||||
|
||||
// ViewModel
|
||||
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
implementation(compose.html.core)
|
||||
}
|
||||
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose.experimental {
|
||||
web.application {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import at.mocode.ui.screens.PersonListScreen
|
||||
import at.mocode.ui.screens.CreatePersonScreen
|
||||
import at.mocode.ui.theme.MeldestelleTheme
|
||||
import at.mocode.di.AppDependencies
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun App() {
|
||||
MeldestelleTheme {
|
||||
val navController = rememberNavController()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Meldestelle - Reitersport Management") }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "person_list",
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
composable("person_list") {
|
||||
val viewModel = remember { AppDependencies.personListViewModel() }
|
||||
PersonListScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToCreatePerson = {
|
||||
navController.navigate("create_person")
|
||||
}
|
||||
)
|
||||
}
|
||||
composable("create_person") {
|
||||
val viewModel = remember { AppDependencies.createPersonViewModel() }
|
||||
CreatePersonScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package at.mocode.di
|
||||
|
||||
import at.mocode.members.application.usecase.CreatePersonUseCase
|
||||
import at.mocode.members.domain.repository.PersonRepository
|
||||
import at.mocode.members.domain.repository.VereinRepository
|
||||
import at.mocode.members.domain.service.MasterDataService
|
||||
import at.mocode.ui.viewmodel.CreatePersonViewModel
|
||||
import at.mocode.ui.viewmodel.PersonListViewModel
|
||||
|
||||
/**
|
||||
* Simple dependency injection container for the application.
|
||||
* In a real application, you might want to use a proper DI framework like Koin.
|
||||
*/
|
||||
object AppDependencies {
|
||||
|
||||
// Mock implementations for demonstration
|
||||
// In a real application, these would be proper implementations
|
||||
private val mockPersonRepository = object : PersonRepository {
|
||||
override suspend fun save(person: at.mocode.members.domain.model.DomPerson): at.mocode.members.domain.model.DomPerson {
|
||||
// Mock implementation - just return the person with an ID
|
||||
return person.copy(id = com.benasher44.uuid.uuid4())
|
||||
}
|
||||
|
||||
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomPerson? {
|
||||
return null // Mock implementation
|
||||
}
|
||||
|
||||
override suspend fun findByOepsSatzNr(oepsSatzNr: String): at.mocode.members.domain.model.DomPerson? {
|
||||
return null // Mock implementation
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
|
||||
return false // Mock implementation - no duplicates for demo
|
||||
}
|
||||
|
||||
override suspend fun findAll(): List<at.mocode.members.domain.model.DomPerson> {
|
||||
return emptyList() // Mock implementation
|
||||
}
|
||||
|
||||
override suspend fun delete(id: com.benasher44.uuid.Uuid) {
|
||||
// Mock implementation
|
||||
}
|
||||
}
|
||||
|
||||
private val mockVereinRepository = object : VereinRepository {
|
||||
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomVerein? {
|
||||
return null // Mock implementation
|
||||
}
|
||||
|
||||
override suspend fun existsById(id: com.benasher44.uuid.Uuid): Boolean {
|
||||
return true // Mock implementation - assume all clubs exist
|
||||
}
|
||||
|
||||
override suspend fun findAll(): List<at.mocode.members.domain.model.DomVerein> {
|
||||
return emptyList() // Mock implementation
|
||||
}
|
||||
}
|
||||
|
||||
private val mockMasterDataService = object : MasterDataService {
|
||||
override suspend fun countryExists(countryId: com.benasher44.uuid.Uuid): Boolean {
|
||||
return true // Mock implementation - assume all countries exist
|
||||
}
|
||||
}
|
||||
|
||||
// Use case instances
|
||||
private val createPersonUseCase = CreatePersonUseCase(
|
||||
personRepository = mockPersonRepository,
|
||||
vereinRepository = mockVereinRepository,
|
||||
masterDataService = mockMasterDataService
|
||||
)
|
||||
|
||||
// ViewModel factory methods
|
||||
fun createPersonViewModel(): CreatePersonViewModel {
|
||||
return CreatePersonViewModel(createPersonUseCase)
|
||||
}
|
||||
|
||||
fun personListViewModel(): PersonListViewModel {
|
||||
return PersonListViewModel(mockPersonRepository)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package at.mocode.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.enums.GeschlechtE
|
||||
import at.mocode.ui.viewmodel.CreatePersonViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreatePersonScreen(
|
||||
viewModel: CreatePersonViewModel,
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
var showGeschlechtDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
// Handle success navigation
|
||||
LaunchedEffect(viewModel.isSuccess) {
|
||||
if (viewModel.isSuccess) {
|
||||
onNavigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Person erstellen") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Error message
|
||||
viewModel.errorMessage?.let { error ->
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = error,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic Information Section
|
||||
Text(
|
||||
text = "Grunddaten",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.nachname,
|
||||
onValueChange = viewModel::updateNachname,
|
||||
label = { Text("Nachname *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.vorname,
|
||||
onValueChange = viewModel::updateVorname,
|
||||
label = { Text("Vorname *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.titel,
|
||||
onValueChange = viewModel::updateTitel,
|
||||
label = { Text("Titel") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
placeholder = { Text("z.B. Dr., Ing.") }
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.oepsSatzNr,
|
||||
onValueChange = viewModel::updateOepsSatzNr,
|
||||
label = { Text("OEPS Satznummer") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
placeholder = { Text("6-stellige Nummer") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.geburtsdatum,
|
||||
onValueChange = viewModel::updateGeburtsdatum,
|
||||
label = { Text("Geburtsdatum") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
placeholder = { Text("YYYY-MM-DD") }
|
||||
)
|
||||
|
||||
// Gender Dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = showGeschlechtDropdown,
|
||||
onExpandedChange = { showGeschlechtDropdown = !showGeschlechtDropdown }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.geschlecht?.let {
|
||||
when(it) {
|
||||
GeschlechtE.M -> "Männlich"
|
||||
GeschlechtE.W -> "Weiblich"
|
||||
GeschlechtE.D -> "Divers"
|
||||
GeschlechtE.UNBEKANNT -> "Unbekannt"
|
||||
}
|
||||
} ?: "",
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
label = { Text("Geschlecht") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showGeschlechtDropdown) },
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = showGeschlechtDropdown,
|
||||
onDismissRequest = { showGeschlechtDropdown = false }
|
||||
) {
|
||||
GeschlechtE.entries.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(when(option) {
|
||||
GeschlechtE.M -> "Männlich"
|
||||
GeschlechtE.W -> "Weiblich"
|
||||
GeschlechtE.D -> "Divers"
|
||||
GeschlechtE.UNBEKANNT -> "Unbekannt"
|
||||
})
|
||||
},
|
||||
onClick = {
|
||||
viewModel.updateGeschlecht(option)
|
||||
showGeschlechtDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contact Information Section
|
||||
Text(
|
||||
text = "Kontaktdaten",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.telefon,
|
||||
onValueChange = viewModel::updateTelefon,
|
||||
label = { Text("Telefon") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.email,
|
||||
onValueChange = viewModel::updateEmail,
|
||||
label = { Text("E-Mail") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||
)
|
||||
|
||||
// Address Section
|
||||
Text(
|
||||
text = "Adresse",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.strasse,
|
||||
onValueChange = viewModel::updateStrasse,
|
||||
label = { Text("Straße und Hausnummer") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.plz,
|
||||
onValueChange = viewModel::updatePlz,
|
||||
label = { Text("PLZ") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.ort,
|
||||
onValueChange = viewModel::updateOrt,
|
||||
label = { Text("Ort") },
|
||||
modifier = Modifier.weight(2f),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.adresszusatz,
|
||||
onValueChange = viewModel::updateAdresszusatz,
|
||||
label = { Text("Adresszusatz") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Additional Information Section
|
||||
Text(
|
||||
text = "Weitere Informationen",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.feiId,
|
||||
onValueChange = viewModel::updateFeiId,
|
||||
label = { Text("FEI ID") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.mitgliedsNummer,
|
||||
onValueChange = viewModel::updateMitgliedsNummer,
|
||||
label = { Text("Mitgliedsnummer beim Stammverein") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = viewModel.istGesperrt,
|
||||
onCheckedChange = viewModel::updateIstGesperrt
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Person ist gesperrt")
|
||||
}
|
||||
|
||||
if (viewModel.istGesperrt) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.sperrGrund,
|
||||
onValueChange = viewModel::updateSperrGrund,
|
||||
label = { Text("Sperrgrund") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 3
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.notizen,
|
||||
onValueChange = viewModel::updateNotizen,
|
||||
label = { Text("Interne Notizen") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 4
|
||||
)
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !viewModel.isLoading
|
||||
) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.createPerson()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !viewModel.isLoading
|
||||
) {
|
||||
if (viewModel.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("Erstellen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package at.mocode.ui.screens
|
||||
|
||||
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.Add
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.members.domain.model.DomPerson
|
||||
import at.mocode.enums.GeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import at.mocode.ui.viewmodel.PersonListViewModel
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PersonListScreen(
|
||||
viewModel: PersonListViewModel,
|
||||
onNavigateToCreatePerson: () -> Unit
|
||||
) {
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onNavigateToCreatePerson
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Person hinzufügen")
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Personen",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// Error handling
|
||||
viewModel.errorMessage?.let { error ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(
|
||||
onClick = { viewModel.clearError() }
|
||||
) {
|
||||
Text("OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (viewModel.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewModel.isLoading && viewModel.persons.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Keine Personen vorhanden",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(viewModel.persons) { person ->
|
||||
PersonCard(person = person)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PersonCard(person: DomPerson) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "${person.titel?.let { "$it " } ?: ""}${person.vorname} ${person.nachname}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
person.oepsSatzNr?.let { oepsNr ->
|
||||
Text(
|
||||
text = "OEPS: $oepsNr",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
person.geburtsdatum?.let { birthDate ->
|
||||
Text(
|
||||
text = "Geboren: $birthDate",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = when (person.datenQuelle) {
|
||||
DatenQuelleE.OEPS_ZNS -> MaterialTheme.colorScheme.primaryContainer
|
||||
DatenQuelleE.MANUELL -> MaterialTheme.colorScheme.secondaryContainer
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = when (person.datenQuelle) {
|
||||
DatenQuelleE.OEPS_ZNS -> "OEPS"
|
||||
DatenQuelleE.MANUELL -> "Manuell"
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = when (person.datenQuelle) {
|
||||
DatenQuelleE.OEPS_ZNS -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
DatenQuelleE.MANUELL -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
person.email?.let { email ->
|
||||
Text(
|
||||
text = "📧 $email",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
person.telefon?.let { phone ->
|
||||
Text(
|
||||
text = "📞 $phone",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
if (person.strasse != null && person.plz != null && person.ort != null) {
|
||||
Text(
|
||||
text = "📍 ${person.strasse}, ${person.plz} ${person.ort}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package at.mocode.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Color(0xFF6750A4),
|
||||
secondary = Color(0xFF625B71),
|
||||
tertiary = Color(0xFF7D5260),
|
||||
background = Color(0xFF1C1B1F),
|
||||
surface = Color(0xFF1C1B1F),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFFFEFBFF),
|
||||
onSurface = Color(0xFFFEFBFF),
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFF6750A4),
|
||||
secondary = Color(0xFF625B71),
|
||||
tertiary = Color(0xFF7D5260),
|
||||
background = Color(0xFFFEFBFF),
|
||||
surface = Color(0xFFFEFBFF),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MeldestelleTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography(),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package at.mocode.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.members.application.usecase.CreatePersonUseCase
|
||||
import at.mocode.members.domain.repository.PersonRepository
|
||||
import at.mocode.members.domain.repository.VereinRepository
|
||||
import at.mocode.members.domain.service.MasterDataService
|
||||
import at.mocode.enums.GeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
class CreatePersonViewModel(
|
||||
private val createPersonUseCase: CreatePersonUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// Form state
|
||||
var nachname by mutableStateOf("")
|
||||
private set
|
||||
var vorname by mutableStateOf("")
|
||||
private set
|
||||
var titel by mutableStateOf("")
|
||||
private set
|
||||
var oepsSatzNr by mutableStateOf("")
|
||||
private set
|
||||
var geburtsdatum by mutableStateOf("")
|
||||
private set
|
||||
var geschlecht by mutableStateOf<GeschlechtE?>(null)
|
||||
private set
|
||||
var telefon by mutableStateOf("")
|
||||
private set
|
||||
var email by mutableStateOf("")
|
||||
private set
|
||||
var strasse by mutableStateOf("")
|
||||
private set
|
||||
var plz by mutableStateOf("")
|
||||
private set
|
||||
var ort by mutableStateOf("")
|
||||
private set
|
||||
var adresszusatz by mutableStateOf("")
|
||||
private set
|
||||
var feiId by mutableStateOf("")
|
||||
private set
|
||||
var mitgliedsNummer by mutableStateOf("")
|
||||
private set
|
||||
var notizen by mutableStateOf("")
|
||||
private set
|
||||
var istGesperrt by mutableStateOf(false)
|
||||
private set
|
||||
var sperrGrund by mutableStateOf("")
|
||||
private set
|
||||
|
||||
// UI state
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var isSuccess by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
// Update methods
|
||||
fun updateNachname(value: String) { nachname = value }
|
||||
fun updateVorname(value: String) { vorname = value }
|
||||
fun updateTitel(value: String) { titel = value }
|
||||
fun updateOepsSatzNr(value: String) { oepsSatzNr = value }
|
||||
fun updateGeburtsdatum(value: String) { geburtsdatum = value }
|
||||
fun updateGeschlecht(value: GeschlechtE?) { geschlecht = value }
|
||||
fun updateTelefon(value: String) { telefon = value }
|
||||
fun updateEmail(value: String) { email = value }
|
||||
fun updateStrasse(value: String) { strasse = value }
|
||||
fun updatePlz(value: String) { plz = value }
|
||||
fun updateOrt(value: String) { ort = value }
|
||||
fun updateAdresszusatz(value: String) { adresszusatz = value }
|
||||
fun updateFeiId(value: String) { feiId = value }
|
||||
fun updateMitgliedsNummer(value: String) { mitgliedsNummer = value }
|
||||
fun updateNotizen(value: String) { notizen = value }
|
||||
fun updateIstGesperrt(value: Boolean) { istGesperrt = value }
|
||||
fun updateSperrGrund(value: String) { sperrGrund = value }
|
||||
|
||||
fun clearError() {
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun createPerson() {
|
||||
// Basic validation
|
||||
when {
|
||||
nachname.isBlank() -> {
|
||||
errorMessage = "Nachname ist erforderlich"
|
||||
return
|
||||
}
|
||||
vorname.isBlank() -> {
|
||||
errorMessage = "Vorname ist erforderlich"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
try {
|
||||
// Parse birth date if provided
|
||||
val parsedGeburtsdatum = if (geburtsdatum.isNotBlank()) {
|
||||
try {
|
||||
val parts = geburtsdatum.split("-")
|
||||
if (parts.size == 3) {
|
||||
LocalDate(parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD"
|
||||
isLoading = false
|
||||
return@launch
|
||||
}
|
||||
} else null
|
||||
|
||||
val request = CreatePersonUseCase.CreatePersonRequest(
|
||||
oepsSatzNr = oepsSatzNr.takeIf { it.isNotBlank() },
|
||||
nachname = nachname,
|
||||
vorname = vorname,
|
||||
titel = titel.takeIf { it.isNotBlank() },
|
||||
geburtsdatum = parsedGeburtsdatum,
|
||||
geschlechtE = geschlecht,
|
||||
telefon = telefon.takeIf { it.isNotBlank() },
|
||||
email = email.takeIf { it.isNotBlank() },
|
||||
strasse = strasse.takeIf { it.isNotBlank() },
|
||||
plz = plz.takeIf { it.isNotBlank() },
|
||||
ort = ort.takeIf { it.isNotBlank() },
|
||||
adresszusatzZusatzinfo = adresszusatz.takeIf { it.isNotBlank() },
|
||||
feiId = feiId.takeIf { it.isNotBlank() },
|
||||
mitgliedsNummerBeiStammVerein = mitgliedsNummer.takeIf { it.isNotBlank() },
|
||||
istGesperrt = istGesperrt,
|
||||
sperrGrund = sperrGrund.takeIf { it.isNotBlank() },
|
||||
datenQuelle = DatenQuelleE.MANUELL,
|
||||
notizenIntern = notizen.takeIf { it.isNotBlank() }
|
||||
)
|
||||
|
||||
val response = createPersonUseCase.execute(request)
|
||||
|
||||
if (response.success) {
|
||||
isSuccess = true
|
||||
} else {
|
||||
errorMessage = response.error?.message ?: "Unbekannter Fehler beim Erstellen der Person"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Fehler beim Erstellen der Person: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetForm() {
|
||||
nachname = ""
|
||||
vorname = ""
|
||||
titel = ""
|
||||
oepsSatzNr = ""
|
||||
geburtsdatum = ""
|
||||
geschlecht = null
|
||||
telefon = ""
|
||||
email = ""
|
||||
strasse = ""
|
||||
plz = ""
|
||||
ort = ""
|
||||
adresszusatz = ""
|
||||
feiId = ""
|
||||
mitgliedsNummer = ""
|
||||
notizen = ""
|
||||
istGesperrt = false
|
||||
sperrGrund = ""
|
||||
isLoading = false
|
||||
errorMessage = null
|
||||
isSuccess = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package at.mocode.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.members.domain.model.DomPerson
|
||||
import at.mocode.members.domain.repository.PersonRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PersonListViewModel(
|
||||
private val personRepository: PersonRepository
|
||||
) : ViewModel() {
|
||||
|
||||
// UI state
|
||||
var persons by mutableStateOf<List<DomPerson>>(emptyList())
|
||||
private set
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
loadPersons()
|
||||
}
|
||||
|
||||
fun loadPersons() {
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
try {
|
||||
persons = personRepository.findAll()
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Fehler beim Laden der Personen: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun refreshPersons() {
|
||||
loadPersons()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package at.mocode.ui.viewmodel
|
||||
|
||||
import at.mocode.members.application.usecase.CreatePersonUseCase
|
||||
import at.mocode.members.domain.model.DomPerson
|
||||
import at.mocode.members.domain.repository.PersonRepository
|
||||
import at.mocode.members.domain.repository.VereinRepository
|
||||
import at.mocode.members.domain.service.MasterDataService
|
||||
import at.mocode.enums.GeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import at.mocode.validation.ValidationResult
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.test.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class CreatePersonViewModelTest {
|
||||
|
||||
private lateinit var mockPersonRepository: PersonRepository
|
||||
private lateinit var mockVereinRepository: VereinRepository
|
||||
private lateinit var mockMasterDataService: MasterDataService
|
||||
private lateinit var createPersonUseCase: CreatePersonUseCase
|
||||
private lateinit var viewModel: CreatePersonViewModel
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
|
||||
mockPersonRepository = object : PersonRepository {
|
||||
private val persons = mutableListOf<DomPerson>()
|
||||
|
||||
override suspend fun save(person: DomPerson): DomPerson {
|
||||
val savedPerson = person.copy(id = uuid4())
|
||||
persons.add(savedPerson)
|
||||
return savedPerson
|
||||
}
|
||||
|
||||
override suspend fun findById(id: com.benasher44.uuid.Uuid): DomPerson? {
|
||||
return persons.find { it.id == id }
|
||||
}
|
||||
|
||||
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson? {
|
||||
return persons.find { it.oepsSatzNr == oepsSatzNr }
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
|
||||
return persons.any { it.oepsSatzNr == oepsSatzNr }
|
||||
}
|
||||
|
||||
override suspend fun findAll(): List<DomPerson> {
|
||||
return persons.toList()
|
||||
}
|
||||
|
||||
override suspend fun delete(id: com.benasher44.uuid.Uuid) {
|
||||
persons.removeAll { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
mockVereinRepository = object : VereinRepository {
|
||||
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomVerein? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun existsById(id: com.benasher44.uuid.Uuid): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun findAll(): List<at.mocode.members.domain.model.DomVerein> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
mockMasterDataService = object : MasterDataService {
|
||||
override suspend fun countryExists(countryId: com.benasher44.uuid.Uuid): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
createPersonUseCase = CreatePersonUseCase(
|
||||
personRepository = mockPersonRepository,
|
||||
vereinRepository = mockVereinRepository,
|
||||
masterDataService = mockMasterDataService
|
||||
)
|
||||
|
||||
viewModel = CreatePersonViewModel(createPersonUseCase)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
assertEquals("", viewModel.nachname)
|
||||
assertEquals("", viewModel.vorname)
|
||||
assertEquals("", viewModel.titel)
|
||||
assertEquals("", viewModel.oepsSatzNr)
|
||||
assertEquals("", viewModel.geburtsdatum)
|
||||
assertNull(viewModel.geschlecht)
|
||||
assertEquals("", viewModel.telefon)
|
||||
assertEquals("", viewModel.email)
|
||||
assertEquals("", viewModel.strasse)
|
||||
assertEquals("", viewModel.plz)
|
||||
assertEquals("", viewModel.ort)
|
||||
assertEquals("", viewModel.adresszusatz)
|
||||
assertEquals("", viewModel.feiId)
|
||||
assertEquals("", viewModel.mitgliedsNummer)
|
||||
assertEquals("", viewModel.notizen)
|
||||
assertFalse(viewModel.istGesperrt)
|
||||
assertEquals("", viewModel.sperrGrund)
|
||||
assertFalse(viewModel.isLoading)
|
||||
assertNull(viewModel.errorMessage)
|
||||
assertFalse(viewModel.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update methods should change state correctly`() {
|
||||
viewModel.updateNachname("Mustermann")
|
||||
viewModel.updateVorname("Max")
|
||||
viewModel.updateTitel("Dr.")
|
||||
viewModel.updateGeschlecht(GeschlechtE.M)
|
||||
viewModel.updateEmail("max@example.com")
|
||||
viewModel.updateIstGesperrt(true)
|
||||
|
||||
assertEquals("Mustermann", viewModel.nachname)
|
||||
assertEquals("Max", viewModel.vorname)
|
||||
assertEquals("Dr.", viewModel.titel)
|
||||
assertEquals(GeschlechtE.M, viewModel.geschlecht)
|
||||
assertEquals("max@example.com", viewModel.email)
|
||||
assertTrue(viewModel.istGesperrt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createPerson should fail with empty nachname`() = runTest {
|
||||
// Given - empty nachname
|
||||
viewModel.updateVorname("Max")
|
||||
|
||||
// When
|
||||
viewModel.createPerson()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals("Nachname ist erforderlich", viewModel.errorMessage)
|
||||
assertFalse(viewModel.isSuccess)
|
||||
assertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createPerson should fail with empty vorname`() = runTest {
|
||||
// Given - empty vorname
|
||||
viewModel.updateNachname("Mustermann")
|
||||
|
||||
// When
|
||||
viewModel.createPerson()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals("Vorname ist erforderlich", viewModel.errorMessage)
|
||||
assertFalse(viewModel.isSuccess)
|
||||
assertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createPerson should succeed with valid data`() = runTest {
|
||||
// Given
|
||||
viewModel.updateNachname("Mustermann")
|
||||
viewModel.updateVorname("Max")
|
||||
viewModel.updateGeschlecht(GeschlechtE.M)
|
||||
viewModel.updateEmail("max@example.com")
|
||||
|
||||
// When
|
||||
viewModel.createPerson()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(viewModel.isSuccess)
|
||||
assertNull(viewModel.errorMessage)
|
||||
assertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createPerson should handle invalid date format`() = runTest {
|
||||
// Given
|
||||
viewModel.updateNachname("Mustermann")
|
||||
viewModel.updateVorname("Max")
|
||||
viewModel.updateGeburtsdatum("invalid-date")
|
||||
|
||||
// When
|
||||
viewModel.createPerson()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals("Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD", viewModel.errorMessage)
|
||||
assertFalse(viewModel.isSuccess)
|
||||
assertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createPerson should handle valid date format`() = runTest {
|
||||
// Given
|
||||
viewModel.updateNachname("Mustermann")
|
||||
viewModel.updateVorname("Max")
|
||||
viewModel.updateGeburtsdatum("1990-05-15")
|
||||
|
||||
// When
|
||||
viewModel.createPerson()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(viewModel.isSuccess)
|
||||
assertNull(viewModel.errorMessage)
|
||||
assertFalse(viewModel.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resetForm should clear all fields`() {
|
||||
// Given - set some values
|
||||
viewModel.updateNachname("Mustermann")
|
||||
viewModel.updateVorname("Max")
|
||||
viewModel.updateEmail("max@example.com")
|
||||
viewModel.updateIstGesperrt(true)
|
||||
|
||||
// When
|
||||
viewModel.resetForm()
|
||||
|
||||
// Then
|
||||
assertEquals("", viewModel.nachname)
|
||||
assertEquals("", viewModel.vorname)
|
||||
assertEquals("", viewModel.email)
|
||||
assertFalse(viewModel.istGesperrt)
|
||||
assertFalse(viewModel.isLoading)
|
||||
assertNull(viewModel.errorMessage)
|
||||
assertFalse(viewModel.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearError should reset error message`() {
|
||||
// Given - simulate an error
|
||||
viewModel.updateNachname("") // This will cause validation error
|
||||
viewModel.updateVorname("Max")
|
||||
|
||||
runTest {
|
||||
viewModel.createPerson()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
}
|
||||
|
||||
assertNotNull(viewModel.errorMessage)
|
||||
|
||||
// When
|
||||
viewModel.clearError()
|
||||
|
||||
// Then
|
||||
assertNull(viewModel.errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package at.mocode.ui.viewmodel
|
||||
|
||||
import at.mocode.members.domain.model.DomPerson
|
||||
import at.mocode.members.domain.repository.PersonRepository
|
||||
import at.mocode.enums.GeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.test.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PersonListViewModelTest {
|
||||
|
||||
private lateinit var mockPersonRepository: PersonRepository
|
||||
private lateinit var viewModel: PersonListViewModel
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
|
||||
mockPersonRepository = object : PersonRepository {
|
||||
private val persons = mutableListOf<DomPerson>()
|
||||
|
||||
override suspend fun save(person: DomPerson): DomPerson {
|
||||
val savedPerson = person.copy(id = uuid4())
|
||||
persons.add(savedPerson)
|
||||
return savedPerson
|
||||
}
|
||||
|
||||
override suspend fun findById(id: com.benasher44.uuid.Uuid): DomPerson? {
|
||||
return persons.find { it.id == id }
|
||||
}
|
||||
|
||||
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson? {
|
||||
return persons.find { it.oepsSatzNr == oepsSatzNr }
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
|
||||
return persons.any { it.oepsSatzNr == oepsSatzNr }
|
||||
}
|
||||
|
||||
override suspend fun findAll(): List<DomPerson> {
|
||||
return persons.toList()
|
||||
}
|
||||
|
||||
override suspend fun delete(id: com.benasher44.uuid.Uuid) {
|
||||
persons.removeAll { it.id == id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
viewModel = PersonListViewModel(mockPersonRepository)
|
||||
|
||||
assertTrue(viewModel.persons.isEmpty())
|
||||
assertFalse(viewModel.isLoading)
|
||||
assertNull(viewModel.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadPersons should update persons list`() = runTest {
|
||||
// Given
|
||||
val testPerson = DomPerson(
|
||||
nachname = "Test",
|
||||
vorname = "User",
|
||||
geschlechtE = GeschlechtE.M,
|
||||
datenQuelle = DatenQuelleE.MANUELL
|
||||
)
|
||||
mockPersonRepository.save(testPerson)
|
||||
|
||||
// When
|
||||
viewModel = PersonListViewModel(mockPersonRepository)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals(1, viewModel.persons.size)
|
||||
assertEquals("Test", viewModel.persons.first().nachname)
|
||||
assertEquals("User", viewModel.persons.first().vorname)
|
||||
assertFalse(viewModel.isLoading)
|
||||
assertNull(viewModel.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshPersons should reload data`() = runTest {
|
||||
// Given
|
||||
viewModel = PersonListViewModel(mockPersonRepository)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val initialCount = viewModel.persons.size
|
||||
|
||||
// Add a new person to repository
|
||||
val newPerson = DomPerson(
|
||||
nachname = "New",
|
||||
vorname = "Person",
|
||||
geschlechtE = GeschlechtE.W,
|
||||
datenQuelle = DatenQuelleE.MANUELL
|
||||
)
|
||||
mockPersonRepository.save(newPerson)
|
||||
|
||||
// When
|
||||
viewModel.refreshPersons()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals(initialCount + 1, viewModel.persons.size)
|
||||
assertTrue(viewModel.persons.any { it.nachname == "New" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearError should reset error message`() {
|
||||
viewModel = PersonListViewModel(mockPersonRepository)
|
||||
|
||||
// Simulate an error (this would normally happen in a real error scenario)
|
||||
// For testing, we can't easily simulate repository errors with our mock
|
||||
// but we can test the clearError functionality
|
||||
|
||||
viewModel.clearError()
|
||||
assertNull(viewModel.errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Meldestelle - Reitersport Management"
|
||||
) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.window.CanvasBasedWindow
|
||||
import org.jetbrains.skiko.wasm.onWasmReady
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun main() {
|
||||
onWasmReady {
|
||||
CanvasBasedWindow("Meldestelle - Reitersport Management") {
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user