fixing(client-module)

This commit is contained in:
stefan
2025-08-12 17:36:55 +02:00
parent a50b1b3822
commit 23b6708197
42 changed files with 198 additions and 3779 deletions
+24 -18
View File
@@ -7,9 +7,29 @@ plugins {
kotlin {
js(IR) {
browser {
// Konfiguriert den Development-Server und die finalen Bundles.
commonWebpackConfig {
outputFileName = "MeldestelleWebApp.js"
devServer = org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig.DevServer(
open = true,
port = 8081
)
}
webpackTask {
cssSupport {
enabled.set(true)
}
}
runTask {
cssSupport {
enabled.set(true)
}
}
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport {
enabled.set(true)
}
}
}
}
binaries.executable()
@@ -18,23 +38,9 @@ kotlin {
sourceSets {
val jsMain by getting {
dependencies {
// Greift explizit auf den JS-Teil unseres KMP-Moduls zu.
implementation(projects.client.commonUi)
// Stellt die Web-spezifischen (HTML) Teile von Jetpack Compose bereit.
implementation(compose.html.core)
// HTTP client for making requests to the backend
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.client.js)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
}
}
val jsTest by getting {
dependencies {
implementation(libs.kotlin.test)
implementation(project(":client:common-ui"))
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import at.mocode.client.ui.App
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow(canvasElementId = "root") {
App()
}
}
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Meldestelle Pro</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<canvas id="root"></canvas>
<script src="MeldestelleWebApp.js"></script>
</body>
</html>
@@ -1,174 +0,0 @@
package at.mocode.client.web
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.css.*
import kotlinx.coroutines.launch
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class PingResponse(val status: String)
@Composable
fun App() {
var responseStatus by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
val httpClient = remember {
HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
}
Div({
style {
fontFamily("Arial, sans-serif")
padding(20.px)
maxWidth(800.px)
margin("0 auto")
}
}) {
H1({
style {
color(Color.darkblue)
textAlign("center")
marginBottom(30.px)
}
}) {
Text("Meldestelle - Reitersport Management")
}
Div({
style {
textAlign("center")
marginBottom(20.px)
}
}) {
P { Text("Welcome to the Meldestelle Web Application") }
P { Text("Click the button below to test the backend connection") }
}
Div({
style {
textAlign("center")
marginBottom(20.px)
}
}) {
Button({
style {
backgroundColor(Color.lightblue)
color(Color.white)
border(0.px)
padding(10.px, 20.px)
fontSize(16.px)
cursor("pointer")
borderRadius(5.px)
}
onClick {
scope.launch {
try {
isLoading = true
errorMessage = null
responseStatus = null
// Try different potential gateway URLs with correct routing
val gatewayUrls = listOf(
"http://localhost:8080/api/ping/ping", // Correct gateway path
"http://localhost:8080/ping", // Direct service call (fallback)
"http://localhost:8081/api/ping/ping" // Alternative gateway port
)
var success = false
for (url in gatewayUrls) {
try {
val response: HttpResponse = httpClient.get(url)
val responseText = response.bodyAsText()
// Try to parse as JSON first
try {
val pingResponse = Json.decodeFromString<PingResponse>(responseText)
responseStatus = pingResponse.status
success = true
break
} catch (e: Exception) {
// If JSON parsing fails, use the raw response
responseStatus = responseText
success = true
break
}
} catch (e: Exception) {
// Continue to next URL
continue
}
}
if (!success) {
errorMessage = "Could not reach any backend service. Please ensure the backend is running."
}
} catch (e: Exception) {
errorMessage = "Error: ${e.message}"
} finally {
isLoading = false
}
}
}
disabled(isLoading)
}) {
Text(if (isLoading) "Loading..." else "Ping Backend")
}
}
// Response display area
Div({
style {
textAlign("center")
marginTop(20.px)
minHeight(100.px)
border(1.px, LineStyle.Solid, Color.lightgray)
borderRadius(5.px)
padding(20.px)
backgroundColor(Color.lightyellow)
}
}) {
when {
isLoading -> {
P { Text("Sending request to backend...") }
}
errorMessage != null -> {
P({
style {
color(Color.red)
fontWeight("bold")
}
}) {
Text(errorMessage!!)
}
}
responseStatus != null -> {
P({
style {
color(Color.green)
fontWeight("bold")
fontSize(18.px)
}
}) {
Text("Backend Response: $responseStatus")
}
}
else -> {
P { Text("Click the button above to test backend connection") }
}
}
}
}
}
@@ -1,35 +0,0 @@
package at.mocode.client.web.di
import at.mocode.client.common.api.ApiClient
import at.mocode.client.common.repository.ClientEventRepository
import at.mocode.client.common.repository.ClientPersonRepository
import at.mocode.client.common.repository.EventRepository
import at.mocode.client.common.repository.PersonRepository
import at.mocode.client.web.viewmodel.CreatePersonViewModel
import at.mocode.client.web.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 {
// Repository instances
private val personRepository: PersonRepository by lazy { ClientPersonRepository() }
private val eventRepository: EventRepository by lazy { ClientEventRepository() }
// ViewModel factory methods
fun createPersonViewModel(): CreatePersonViewModel {
return CreatePersonViewModel(personRepository)
}
fun personListViewModel(): PersonListViewModel {
return PersonListViewModel(personRepository)
}
// Helper method to initialize dependencies
fun initialize() {
// Initialize ApiClient if needed
println("AppDependencies initialized")
}
}
@@ -1,10 +0,0 @@
package at.mocode.client.web
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.renderComposable
fun main() {
renderComposable(rootElementId = "root") {
App()
}
}
@@ -1,275 +0,0 @@
package at.mocode.client.web.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.automirrored.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.client.web.viewmodel.CreatePersonViewModel
/**
* Screen for creating a new person.
* This is a simplified version that uses the simplified CreatePersonViewModel.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreatePersonScreen(
viewModel: CreatePersonViewModel,
onNavigateBack: () -> Unit
) {
// Handle success navigation
LaunchedEffect(viewModel.isSuccess) {
if (viewModel.isSuccess) {
onNavigateBack()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Person erstellen") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.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") }
)
// 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")
}
}
}
}
}
}
@@ -1,166 +0,0 @@
package at.mocode.client.web.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.client.web.viewmodel.PersonListViewModel
import at.mocode.client.web.viewmodel.PersonUiModel
/**
* Screen for displaying a list of persons.
* This is a simplified version that uses the simplified PersonListViewModel.
*/
@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)
}
}
}
}
}
}
@Composable
private fun PersonCard(person: PersonUiModel) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = person.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
person.email?.let { email ->
Text(
text = "📧 $email",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
person.phone?.let { phone ->
Text(
text = "📞 $phone",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
person.address?.let { address ->
Text(
text = "📍 $address",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@@ -1,181 +0,0 @@
package at.mocode.client.web.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import at.mocode.client.common.repository.Person
import at.mocode.client.common.repository.PersonRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
/**
* ViewModel for creating a person.
* This is a simplified version that doesn't depend on androidx.lifecycle.
* It uses Compose for Desktop's own state management.
*/
class CreatePersonViewModel(
private val personRepository: PersonRepository
) {
// Coroutine scope for launching background tasks
private val coroutineScope = CoroutineScope(Dispatchers.Default)
// 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 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 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
}
}
coroutineScope.launch {
isLoading = true
errorMessage = null
try {
// Parse birthdate 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 {
errorMessage = "Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD"
isLoading = false
isSuccess = false
return@launch
}
} catch (_: Exception) {
errorMessage = "Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD"
isLoading = false
isSuccess = false
return@launch
}
} else null
// Create a Person object from form data
val person = Person(
nachname = nachname,
vorname = vorname,
titel = titel.takeIf { it.isNotBlank() },
oepsSatzNr = oepsSatzNr.takeIf { it.isNotBlank() },
geburtsdatum = parsedGeburtsdatum,
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() },
adresszusatz = adresszusatz.takeIf { it.isNotBlank() },
feiId = feiId.takeIf { it.isNotBlank() },
mitgliedsNummer = mitgliedsNummer.takeIf { it.isNotBlank() },
notizen = notizen.takeIf { it.isNotBlank() },
istGesperrt = istGesperrt,
sperrGrund = sperrGrund.takeIf { it.isNotBlank() },
datenQuelle = "MANUELL"
)
// Save the person using the repository
personRepository.save(person)
// Set success state
isSuccess = true
} catch (e: Exception) {
errorMessage = "Fehler beim Erstellen der Person: ${e.message}"
} finally {
isLoading = false
}
}
}
fun resetForm() {
nachname = ""
vorname = ""
titel = ""
oepsSatzNr = ""
geburtsdatum = ""
telefon = ""
email = ""
strasse = ""
plz = ""
ort = ""
adresszusatz = ""
feiId = ""
mitgliedsNummer = ""
notizen = ""
istGesperrt = false
sperrGrund = ""
isLoading = false
errorMessage = null
isSuccess = false
}
}
@@ -1,86 +0,0 @@
package at.mocode.client.web.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import at.mocode.client.common.repository.Person
import at.mocode.client.common.repository.PersonRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* ViewModel for displaying a list of persons.
* This is a simplified version that doesn't depend on androidx.lifecycle.
* It uses Compose for Desktop's own state management.
*/
class PersonListViewModel(
private val personRepository: PersonRepository
) {
// Coroutine scope for launching background tasks
private val coroutineScope = CoroutineScope(Dispatchers.Default)
// UI state
var persons by mutableStateOf<List<PersonUiModel>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
init {
loadPersons()
}
fun loadPersons() {
coroutineScope.launch {
isLoading = true
errorMessage = null
try {
// Load persons from the repository
val personList = personRepository.findAllActive(limit = 100, offset = 0)
// Map domain models to UI models
persons = personList.map { it.toUiModel() }
} catch (e: Exception) {
errorMessage = "Fehler beim Laden der Personen: ${e.message}"
} finally {
isLoading = false
}
}
}
fun clearError() {
errorMessage = null
}
fun refreshPersons() {
loadPersons()
}
/**
* Maps a domain Person to a UI PersonUiModel
*/
private fun Person.toUiModel(): PersonUiModel {
return PersonUiModel(
id = this.id,
name = this.getFullName(),
email = this.email,
phone = this.telefon,
address = this.getFormattedAddress()
)
}
}
/**
* UI model for a person.
* This is a simplified version that doesn't depend on domain models.
*/
data class PersonUiModel(
val id: String,
val name: String,
val email: String? = null,
val phone: String? = null,
val address: String? = null
)
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meldestelle - Reitersport Management</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="MeldestelleWebApp.js"></script>
</body>
</html>
@@ -1,258 +0,0 @@
package at.mocode.client.web.viewmodel
import at.mocode.client.common.repository.Person
import at.mocode.client.common.repository.PersonRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import kotlin.test.*
/**
* Simplified test suite for client-side Person functionality.
*
* This test focuses on the client-layer PersonRepository without domain dependencies.
* Tests cover basic CRUD operations through the client repository interface.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class CreatePersonViewModelTest {
private lateinit var mockPersonRepository: PersonRepository
private val testDispatcher = StandardTestDispatcher()
@BeforeTest
fun setup() {
Dispatchers.setMain(testDispatcher)
setupMockRepository()
}
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
/**
* Sets up mock repository for testing
*/
private fun setupMockRepository() {
mockPersonRepository = object : PersonRepository {
private val persons = mutableListOf<Person>()
override suspend fun save(person: Person): Person {
val savedPerson = if (person.id.isBlank()) {
person.copy(id = "test-id-${persons.size + 1}")
} else {
person
}
persons.removeIf { it.id == savedPerson.id }
persons.add(savedPerson)
return savedPerson
}
override suspend fun findById(id: String): Person? {
return persons.find { it.id == id }
}
override suspend fun findByName(searchTerm: String, limit: Int): List<Person> {
return persons.filter {
it.vorname.contains(searchTerm, ignoreCase = true) ||
it.nachname.contains(searchTerm, ignoreCase = true)
}.take(limit)
}
override suspend fun findAllActive(limit: Int, offset: Int): List<Person> {
return persons.filter { !it.istGesperrt }.drop(offset).take(limit)
}
override suspend fun delete(id: String): Boolean {
return persons.removeIf { it.id == id }
}
override suspend fun countActive(): Long {
return persons.filter { !it.istGesperrt }.size.toLong()
}
}
}
@Test
fun `test person repository save creates new person`() = runTest {
// Given
val newPerson = Person(
nachname = "Mustermann",
vorname = "Max",
email = "max@example.com"
)
// When
val savedPerson = mockPersonRepository.save(newPerson)
// Then
assertNotNull(savedPerson.id)
assertTrue(savedPerson.id.isNotBlank())
assertEquals("Mustermann", savedPerson.nachname)
assertEquals("Max", savedPerson.vorname)
assertEquals("max@example.com", savedPerson.email)
}
@Test
fun `test person repository save updates existing person`() = runTest {
// Given
val person = Person(
id = "existing-id",
nachname = "Mustermann",
vorname = "Max",
email = "max@example.com"
)
mockPersonRepository.save(person)
// When
val updatedPerson = person.copy(email = "max.updated@example.com")
val savedPerson = mockPersonRepository.save(updatedPerson)
// Then
assertEquals("existing-id", savedPerson.id)
assertEquals("max.updated@example.com", savedPerson.email)
}
@Test
fun `test person repository findById returns correct person`() = runTest {
// Given
val person = Person(
nachname = "Mustermann",
vorname = "Max",
email = "max@example.com"
)
val savedPerson = mockPersonRepository.save(person)
// When
val foundPerson = mockPersonRepository.findById(savedPerson.id)
// Then
assertNotNull(foundPerson)
assertEquals(savedPerson.id, foundPerson.id)
assertEquals("Mustermann", foundPerson.nachname)
assertEquals("Max", foundPerson.vorname)
}
@Test
fun `test person repository findById returns null for non-existent id`() = runTest {
// When
val foundPerson = mockPersonRepository.findById("non-existent-id")
// Then
assertNull(foundPerson)
}
@Test
fun `test person repository findByName returns matching persons`() = runTest {
// Given
val person1 = Person(nachname = "Mustermann", vorname = "Max")
val person2 = Person(nachname = "Schmidt", vorname = "Anna")
val person3 = Person(nachname = "Mueller", vorname = "Max")
mockPersonRepository.save(person1)
mockPersonRepository.save(person2)
mockPersonRepository.save(person3)
// When
val foundPersons = mockPersonRepository.findByName("Max", 10)
// Then
assertEquals(2, foundPersons.size)
assertTrue(foundPersons.any { it.vorname == "Max" && it.nachname == "Mustermann" })
assertTrue(foundPersons.any { it.vorname == "Max" && it.nachname == "Mueller" })
}
@Test
fun `test person repository findAllActive returns only active persons`() = runTest {
// Given
val activePerson = Person(nachname = "Active", vorname = "Person", istGesperrt = false)
val blockedPerson = Person(nachname = "Blocked", vorname = "Person", istGesperrt = true)
mockPersonRepository.save(activePerson)
mockPersonRepository.save(blockedPerson)
// When
val activePersons = mockPersonRepository.findAllActive(10, 0)
// Then
assertEquals(1, activePersons.size)
assertEquals("Active", activePersons.first().nachname)
assertFalse(activePersons.first().istGesperrt)
}
@Test
fun `test person repository delete removes person`() = runTest {
// Given
val person = Person(nachname = "ToDelete", vorname = "Person")
val savedPerson = mockPersonRepository.save(person)
// When
val deleted = mockPersonRepository.delete(savedPerson.id)
// Then
assertTrue(deleted)
assertNull(mockPersonRepository.findById(savedPerson.id))
}
@Test
fun `test person repository countActive returns correct count`() = runTest {
// Given
val activePerson1 = Person(nachname = "Active1", vorname = "Person", istGesperrt = false)
val activePerson2 = Person(nachname = "Active2", vorname = "Person", istGesperrt = false)
val blockedPerson = Person(nachname = "Blocked", vorname = "Person", istGesperrt = true)
mockPersonRepository.save(activePerson1)
mockPersonRepository.save(activePerson2)
mockPersonRepository.save(blockedPerson)
// When
val count = mockPersonRepository.countActive()
// Then
assertEquals(2L, count)
}
@Test
fun `test person getFullName method`() {
// Given
val personWithTitle = Person(
nachname = "Mustermann",
vorname = "Max",
titel = "Dr."
)
val personWithoutTitle = Person(
nachname = "Schmidt",
vorname = "Anna"
)
// When & Then
assertEquals("Dr. Max Mustermann", personWithTitle.getFullName())
assertEquals("Anna Schmidt", personWithoutTitle.getFullName())
}
@Test
fun `test person getFormattedAddress method`() {
// Given
val personWithCompleteAddress = Person(
nachname = "Mustermann",
vorname = "Max",
strasse = "Musterstraße 123",
plz = "12345",
ort = "Musterstadt",
adresszusatz = "2. Stock"
)
val personWithIncompleteAddress = Person(
nachname = "Schmidt",
vorname = "Anna",
strasse = "Teststraße 456"
// Missing PLZ and Ort
)
// When & Then
assertEquals("Musterstraße 123, 2. Stock, 12345 Musterstadt", personWithCompleteAddress.getFormattedAddress())
assertNull(personWithIncompleteAddress.getFormattedAddress())
}
}