fixing(client-module)
This commit is contained in:
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-181
@@ -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>
|
||||
-258
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user