fixing(client-module)
This commit is contained in:
@@ -1,63 +1,45 @@
|
|||||||
plugins {
|
plugins {
|
||||||
// KORREKTUR: Wir deklarieren dieses Modul als Kotlin Multiplatform Modul.
|
|
||||||
alias(libs.plugins.kotlin.multiplatform)
|
|
||||||
// KORREKTUR: Wir deklarieren, dass wir Jetpack Compose verwenden.
|
|
||||||
alias(libs.plugins.compose.multiplatform)
|
alias(libs.plugins.compose.multiplatform)
|
||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Wir definieren die Zielplattformen, für die dieses Modul Code bereitstellt.
|
jvm("desktop")
|
||||||
jvm("desktop") // Ein JVM-Target für unsere Desktop-App
|
js(IR) {
|
||||||
js(IR) { // Ein JavaScript-Target für unsere Web-App
|
|
||||||
browser()
|
browser()
|
||||||
binaries.executable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hier definieren wir die Abhängigkeiten für die jeweiligen Source Sets.
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
// --- Interne Module (für alle Plattformen verfügbar) ---
|
// Compose UI
|
||||||
api(projects.core.coreDomain)
|
|
||||||
api(projects.core.coreUtils)
|
|
||||||
|
|
||||||
// --- Jetpack Compose UI (für alle Plattformen verfügbar) ---
|
|
||||||
api(compose.runtime)
|
api(compose.runtime)
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
api(compose.material3)
|
api(compose.material3)
|
||||||
api(compose.ui)
|
|
||||||
api(compose.components.resources)
|
|
||||||
api(compose.materialIconsExtended)
|
api(compose.materialIconsExtended)
|
||||||
|
|
||||||
// --- Ktor Client für API-Kommunikation (Kernmodul für alle) ---
|
// Ktor Client for API calls
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.contentNegotiation)
|
implementation(libs.ktor.client.contentNegotiation)
|
||||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||||
|
|
||||||
// --- Coroutines (für alle Plattformen verfügbar) ---
|
// Coroutines for background tasks
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val desktopMain by getting {
|
val desktopMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Ktor-Engine, die nur für die Desktop (JVM) Version benötigt wird
|
// Ktor engine for Desktop
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Ktor-Engine, die nur für die Web (JS) Version benötigt wird
|
// Ktor engine for Browser
|
||||||
implementation(libs.ktor.client.js)
|
implementation(libs.ktor.client.js)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val commonTest by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(libs.kotlin.test)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package at.mocode.client.data.api
|
||||||
|
|
||||||
|
import at.mocode.client.data.model.PingResponse
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class PingApiClient {
|
||||||
|
private val httpClient = HttpClient {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun ping(): String {
|
||||||
|
return try {
|
||||||
|
// HINWEIS: Wir rufen hier Port 8081 an, den Port unseres Gateways.
|
||||||
|
val response = httpClient.get("http://localhost:8081/ping").body<PingResponse>()
|
||||||
|
response.status
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"Fehler: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package at.mocode.client.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PingResponse(
|
||||||
|
val status: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package at.mocode.client.ui
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import at.mocode.client.ui.features.ping.PingScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun App() {
|
||||||
|
MaterialTheme {
|
||||||
|
PingScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package at.mocode.client.ui.features.ping
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PingScreen() {
|
||||||
|
val viewModel = remember { PingViewModel() }
|
||||||
|
val responseText by viewModel.responseText.collectAsState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Button(onClick = { viewModel.onPingClicked() }) {
|
||||||
|
Text("Ping Backend")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = responseText,
|
||||||
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package at.mocode.client.ui.features.ping
|
||||||
|
|
||||||
|
import at.mocode.client.data.api.PingApiClient
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PingViewModel {
|
||||||
|
private val apiClient = PingApiClient()
|
||||||
|
private val viewModelScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
|
||||||
|
private val _responseText = MutableStateFlow("Klicke auf den Button, um das Backend zu pingen.")
|
||||||
|
val responseText = _responseText.asStateFlow()
|
||||||
|
|
||||||
|
fun onPingClicked() {
|
||||||
|
_responseText.value = "Pinge Backend..."
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = apiClient.ping()
|
||||||
|
_responseText.value = "Antwort vom Backend: $response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package at.mocode.client.common
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import at.mocode.client.common.theme.MeldestelleTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base application theme wrapper for consistent UI across all applications.
|
|
||||||
* This is a simplified version that just applies the theme.
|
|
||||||
* Specific applications should implement their own App composable with navigation.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun BaseApp(content: @Composable () -> Unit) {
|
|
||||||
MeldestelleTheme {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
package at.mocode.client.common.api
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
|
||||||
import at.mocode.core.domain.model.ErrorDto
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.engine.cio.*
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.statement.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared API client for making HTTP requests to the backend API.
|
|
||||||
* Provides methods for common HTTP operations and handles response deserialization.
|
|
||||||
* Includes a simple caching mechanism for GET requests.
|
|
||||||
*/
|
|
||||||
object ApiClient {
|
|
||||||
// Public properties to avoid inline function issues
|
|
||||||
val BASE_URL = "http://localhost:8080"
|
|
||||||
val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
|
||||||
|
|
||||||
val httpClient = HttpClient(CIO) {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add error handling, timeouts, etc.
|
|
||||||
engine {
|
|
||||||
requestTimeout = 30_000 // 30 seconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache implementation
|
|
||||||
val cache = ConcurrentHashMap<String, Pair<Any, Long>>()
|
|
||||||
val CACHE_TTL = 30_000L // 30 seconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic GET method with ApiResponse handling and caching
|
|
||||||
*
|
|
||||||
* @param endpoint The API endpoint to call (without base URL)
|
|
||||||
* @param cacheable Whether to cache the response
|
|
||||||
* @return The deserialized data of type T
|
|
||||||
* @throws ApiException if the request fails or returns an error
|
|
||||||
*/
|
|
||||||
suspend inline fun <reified T> get(endpoint: String, cacheable: Boolean = true): T? {
|
|
||||||
try {
|
|
||||||
// Check cache if cacheable
|
|
||||||
if (cacheable) {
|
|
||||||
val cacheKey = endpoint
|
|
||||||
val cachedValue = cache[cacheKey]
|
|
||||||
if (cachedValue != null && System.currentTimeMillis() - cachedValue.second < CACHE_TTL) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return cachedValue.first as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make HTTP request
|
|
||||||
val response = httpClient.get("$BASE_URL$endpoint")
|
|
||||||
val responseText = response.bodyAsText()
|
|
||||||
val apiResponse = json.decodeFromString<ApiResponse<T>>(responseText)
|
|
||||||
|
|
||||||
// Handle success/error
|
|
||||||
if (apiResponse.success) {
|
|
||||||
val data = apiResponse.data
|
|
||||||
|
|
||||||
// Update cache if cacheable
|
|
||||||
if (cacheable && data != null) {
|
|
||||||
val cacheKey = endpoint
|
|
||||||
cache[cacheKey] = Pair(data, System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
} else {
|
|
||||||
throw ApiException(
|
|
||||||
message = apiResponse.error?.message ?: "Unknown API error",
|
|
||||||
code = apiResponse.error?.code ?: "ERROR",
|
|
||||||
details = apiResponse.error?.details
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is ApiException) throw e
|
|
||||||
throw ApiException(
|
|
||||||
message = "Error executing GET request: ${e.message}",
|
|
||||||
code = "ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic POST method with ApiResponse handling
|
|
||||||
*
|
|
||||||
* @param endpoint The API endpoint to call (without base URL)
|
|
||||||
* @param body The request body to send
|
|
||||||
* @return The deserialized data of type T
|
|
||||||
* @throws ApiException if the request fails or returns an error
|
|
||||||
*/
|
|
||||||
suspend inline fun <reified T> post(endpoint: String, body: Any): T {
|
|
||||||
try {
|
|
||||||
// Make HTTP request
|
|
||||||
val response = httpClient.post("$BASE_URL$endpoint") {
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
setBody(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseText = response.bodyAsText()
|
|
||||||
val apiResponse = json.decodeFromString<ApiResponse<T>>(responseText)
|
|
||||||
|
|
||||||
// Handle success/error
|
|
||||||
if (apiResponse.success) {
|
|
||||||
return apiResponse.data
|
|
||||||
?: throw IllegalStateException("API response success but data is null")
|
|
||||||
} else {
|
|
||||||
throw ApiException(
|
|
||||||
message = apiResponse.error?.message ?: "Unknown API error",
|
|
||||||
code = apiResponse.error?.code ?: "ERROR",
|
|
||||||
details = apiResponse.error?.details
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is ApiException) throw e
|
|
||||||
throw ApiException(
|
|
||||||
message = "Error executing POST request: ${e.message}",
|
|
||||||
code = "ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic PUT method with ApiResponse handling
|
|
||||||
*
|
|
||||||
* @param endpoint The API endpoint to call (without base URL)
|
|
||||||
* @param body The request body to send
|
|
||||||
* @return The deserialized data of type T
|
|
||||||
* @throws ApiException if the request fails or returns an error
|
|
||||||
*/
|
|
||||||
suspend inline fun <reified T> put(endpoint: String, body: Any): T {
|
|
||||||
try {
|
|
||||||
// Make HTTP request
|
|
||||||
val response = httpClient.put("$BASE_URL$endpoint") {
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
setBody(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseText = response.bodyAsText()
|
|
||||||
val apiResponse = json.decodeFromString<ApiResponse<T>>(responseText)
|
|
||||||
|
|
||||||
// Handle success/error
|
|
||||||
if (apiResponse.success) {
|
|
||||||
return apiResponse.data
|
|
||||||
?: throw IllegalStateException("API response success but data is null")
|
|
||||||
} else {
|
|
||||||
throw ApiException(
|
|
||||||
message = apiResponse.error?.message ?: "Unknown API error",
|
|
||||||
code = apiResponse.error?.code ?: "ERROR",
|
|
||||||
details = apiResponse.error?.details
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is ApiException) throw e
|
|
||||||
throw ApiException(
|
|
||||||
message = "Error executing PUT request: ${e.message}",
|
|
||||||
code = "ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic DELETE method with ApiResponse handling
|
|
||||||
*
|
|
||||||
* @param endpoint The API endpoint to call (without base URL)
|
|
||||||
* @return The deserialized data of type T
|
|
||||||
* @throws ApiException if the request fails or returns an error
|
|
||||||
*/
|
|
||||||
suspend inline fun <reified T> delete(endpoint: String): T {
|
|
||||||
try {
|
|
||||||
// Make HTTP request
|
|
||||||
val response = httpClient.delete("$BASE_URL$endpoint")
|
|
||||||
|
|
||||||
val responseText = response.bodyAsText()
|
|
||||||
val apiResponse = json.decodeFromString<ApiResponse<T>>(responseText)
|
|
||||||
|
|
||||||
// Handle success/error
|
|
||||||
if (apiResponse.success) {
|
|
||||||
return apiResponse.data
|
|
||||||
?: throw IllegalStateException("API response success but data is null")
|
|
||||||
} else {
|
|
||||||
throw ApiException(
|
|
||||||
message = apiResponse.error?.message ?: "Unknown API error",
|
|
||||||
code = apiResponse.error?.code ?: "ERROR",
|
|
||||||
details = apiResponse.error?.details
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is ApiException) throw e
|
|
||||||
throw ApiException(
|
|
||||||
message = "Error executing DELETE request: ${e.message}",
|
|
||||||
code = "ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the cache
|
|
||||||
*/
|
|
||||||
fun clearCache() {
|
|
||||||
cache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a specific item from the cache
|
|
||||||
*/
|
|
||||||
fun invalidateCache(endpoint: String) {
|
|
||||||
cache.remove(endpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when an API request fails
|
|
||||||
*/
|
|
||||||
class ApiException(
|
|
||||||
message: String,
|
|
||||||
val code: String,
|
|
||||||
val details: Map<String, String>?
|
|
||||||
) : Exception(message)
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package at.mocode.client.common.cache
|
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
|
||||||
import kotlin.concurrent.read
|
|
||||||
import kotlin.concurrent.write
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thread-safe LRU cache implementation for API responses.
|
|
||||||
* Provides TTL-based expiration and size-based eviction.
|
|
||||||
*/
|
|
||||||
class ApiCache(
|
|
||||||
private val maxSize: Int,
|
|
||||||
private val ttlMs: Long
|
|
||||||
) {
|
|
||||||
private val cache = ConcurrentHashMap<String, CacheEntry>()
|
|
||||||
private val accessOrder = ConcurrentLinkedQueue<String>()
|
|
||||||
private val lock = ReentrantReadWriteLock()
|
|
||||||
|
|
||||||
data class CacheEntry(
|
|
||||||
val data: Any,
|
|
||||||
val timestamp: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a cached value if it exists and hasn't expired.
|
|
||||||
*/
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T> get(key: String): T? {
|
|
||||||
return lock.read {
|
|
||||||
val entry = cache[key] ?: return null
|
|
||||||
|
|
||||||
// Check if expired
|
|
||||||
if (System.currentTimeMillis() - entry.timestamp > ttlMs) {
|
|
||||||
// Remove expired entry
|
|
||||||
lock.write {
|
|
||||||
cache.remove(key)
|
|
||||||
accessOrder.remove(key)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update access order
|
|
||||||
lock.write {
|
|
||||||
accessOrder.remove(key)
|
|
||||||
accessOrder.offer(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.data as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores a value in the cache with current timestamp.
|
|
||||||
*/
|
|
||||||
fun put(key: String, value: Any) {
|
|
||||||
lock.write {
|
|
||||||
// Remove if already exists
|
|
||||||
if (cache.containsKey(key)) {
|
|
||||||
accessOrder.remove(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new entry
|
|
||||||
cache[key] = CacheEntry(value, System.currentTimeMillis())
|
|
||||||
accessOrder.offer(key)
|
|
||||||
|
|
||||||
// Evict oldest entries if over capacity
|
|
||||||
while (cache.size > maxSize) {
|
|
||||||
val oldestKey = accessOrder.poll()
|
|
||||||
if (oldestKey != null) {
|
|
||||||
cache.remove(oldestKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a specific entry from the cache.
|
|
||||||
*/
|
|
||||||
fun remove(key: String) {
|
|
||||||
lock.write {
|
|
||||||
cache.remove(key)
|
|
||||||
accessOrder.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes entries matching the given pattern.
|
|
||||||
* Useful for invalidating related cache entries.
|
|
||||||
*/
|
|
||||||
fun removePattern(pattern: String) {
|
|
||||||
lock.write {
|
|
||||||
val keysToRemove = cache.keys.filter { it.contains(pattern) }
|
|
||||||
keysToRemove.forEach { key ->
|
|
||||||
cache.remove(key)
|
|
||||||
accessOrder.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all cached entries.
|
|
||||||
*/
|
|
||||||
fun clear() {
|
|
||||||
lock.write {
|
|
||||||
cache.clear()
|
|
||||||
accessOrder.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all expired entries from the cache.
|
|
||||||
*/
|
|
||||||
fun cleanupExpired() {
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
lock.write {
|
|
||||||
val expiredKeys = cache.entries
|
|
||||||
.filter { currentTime - it.value.timestamp > ttlMs }
|
|
||||||
.map { it.key }
|
|
||||||
|
|
||||||
expiredKeys.forEach { key ->
|
|
||||||
cache.remove(key)
|
|
||||||
accessOrder.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns current cache statistics.
|
|
||||||
*/
|
|
||||||
fun getStats(): CacheStats {
|
|
||||||
return lock.read {
|
|
||||||
CacheStats(
|
|
||||||
size = cache.size,
|
|
||||||
maxSize = maxSize,
|
|
||||||
ttlMs = ttlMs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class CacheStats(
|
|
||||||
val size: Int,
|
|
||||||
val maxSize: Int,
|
|
||||||
val ttlMs: Long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
package at.mocode.client.common.components.events
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import at.mocode.events.domain.model.Veranstaltung
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility functions for event display in Compose UI
|
|
||||||
* This is a Compose-based replacement for the JS-specific EventUIUtils
|
|
||||||
*/
|
|
||||||
object EventComposeUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats an event as a summary string
|
|
||||||
*/
|
|
||||||
fun formatEventSummary(event: Veranstaltung): String {
|
|
||||||
return buildString {
|
|
||||||
append("${event.name}")
|
|
||||||
append(" | ${event.ort}")
|
|
||||||
append(" | ${event.startDatum}")
|
|
||||||
if (event.isMultiDay()) {
|
|
||||||
append(" - ${event.endDatum}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a formatted date range string for an event
|
|
||||||
*/
|
|
||||||
fun formatEventDateRange(event: Veranstaltung): String {
|
|
||||||
return if (event.isMultiDay()) {
|
|
||||||
"${event.startDatum} - ${event.endDatum} (${event.getDurationInDays()} Tage)"
|
|
||||||
} else {
|
|
||||||
"${event.startDatum} (Eintägige Veranstaltung)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of status indicators for an event
|
|
||||||
*/
|
|
||||||
fun getEventStatusList(event: Veranstaltung): List<String> {
|
|
||||||
val statusList = mutableListOf<String>()
|
|
||||||
if (event.istAktiv) statusList.add("Aktiv")
|
|
||||||
if (event.istOeffentlich) statusList.add("Öffentlich")
|
|
||||||
if (event.isRegistrationOpen()) statusList.add("Anmeldung offen")
|
|
||||||
return statusList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A compact event card for displaying basic event information
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CompactEventCard(
|
|
||||||
event: Veranstaltung,
|
|
||||||
onClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
|
||||||
onClick = onClick
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = event.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📍",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = event.ort,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📅",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = EventComposeUtils.formatEventDateRange(event),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status indicators
|
|
||||||
val statusList = EventComposeUtils.getEventStatusList(event)
|
|
||||||
if (statusList.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Status: ${statusList.joinToString(", ")}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A badge that displays the event status
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun EventStatusBadge(event: Veranstaltung) {
|
|
||||||
val statusList = EventComposeUtils.getEventStatusList(event)
|
|
||||||
if (statusList.isNotEmpty()) {
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = statusList.first(),
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-233
@@ -1,233 +0,0 @@
|
|||||||
package at.mocode.client.common.components.events
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
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.events.domain.model.Veranstaltung
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compose component that displays a list of events (Veranstaltungen).
|
|
||||||
* This is a Compose-based replacement for the React-based VeranstaltungsListe component.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun VeranstaltungsListe(
|
|
||||||
events: List<Veranstaltung> = emptyList(),
|
|
||||||
isLoading: Boolean = false,
|
|
||||||
errorMessage: String? = null
|
|
||||||
) {
|
|
||||||
// UI rendering with Compose
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Veranstaltungen",
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
when {
|
|
||||||
isLoading -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errorMessage != null -> {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = errorMessage,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
events.isEmpty() -> {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Keine Veranstaltungen verfügbar",
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
LazyColumn(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(events) { event ->
|
|
||||||
EventCard(event = event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun EventCard(event: Veranstaltung) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = event.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📍",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = event.ort,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📅",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = if (event.isMultiDay()) {
|
|
||||||
"${event.startDatum} - ${event.endDatum} (${event.getDurationInDays()} Tage)"
|
|
||||||
} else {
|
|
||||||
"${event.startDatum} (Eintägige Veranstaltung)"
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status indicators
|
|
||||||
val statusList = mutableListOf<String>()
|
|
||||||
if (event.istAktiv) statusList.add("Aktiv")
|
|
||||||
if (event.istOeffentlich) statusList.add("Öffentlich")
|
|
||||||
if (event.isRegistrationOpen()) statusList.add("Anmeldung offen")
|
|
||||||
|
|
||||||
if (statusList.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "ℹ️",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Status: ${statusList.joinToString(", ")}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
if (!event.beschreibung.isNullOrBlank()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📝",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = event.beschreibung!!,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sports/Sparten
|
|
||||||
if (event.sparten.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🏆",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Sparten: ${event.sparten.joinToString(", ") { it.name }}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional info
|
|
||||||
event.maxTeilnehmer?.let { max ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "👥",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Max. Teilnehmer: $max",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.anmeldeschluss?.let { deadline ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "⏰",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Anmeldeschluss: $deadline",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-237
@@ -1,237 +0,0 @@
|
|||||||
package at.mocode.client.common.components.horses
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
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.horses.domain.model.DomPferd
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compose component that displays a list of horses (Pferde).
|
|
||||||
* This is a Compose-based replacement for the React-based PferdeListe component.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun PferdeListe(
|
|
||||||
horses: List<DomPferd> = emptyList(),
|
|
||||||
isLoading: Boolean = false,
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onHorseClick: (DomPferd) -> Unit = {}
|
|
||||||
) {
|
|
||||||
// UI rendering with Compose
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Pferde-Register",
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
when {
|
|
||||||
isLoading -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errorMessage != null -> {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = errorMessage,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
horses.isEmpty() -> {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Keine Pferde verfügbar",
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
LazyColumn(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(horses) { horse ->
|
|
||||||
HorseCard(horse = horse, onClick = { onHorseClick(horse) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HorseCard(
|
|
||||||
horse: DomPferd,
|
|
||||||
onClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
|
||||||
onClick = onClick
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = horse.getDisplayName(),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Basic information
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🐎",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Geschlecht: ${horse.geschlecht.name}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
horse.geburtsdatum?.let { birthDate ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📅",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = buildString {
|
|
||||||
append("Geburtsdatum: $birthDate")
|
|
||||||
horse.getAge()?.let { age ->
|
|
||||||
append(" (${age} Jahre alt)")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Breed and color
|
|
||||||
val breedAndColor = mutableListOf<String>()
|
|
||||||
horse.rasse?.let { breedAndColor.add("Rasse: $it") }
|
|
||||||
horse.farbe?.let { breedAndColor.add("Farbe: $it") }
|
|
||||||
|
|
||||||
if (breedAndColor.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🏇",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = breedAndColor.joinToString(" | "),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identification numbers (show only the most important ones in the card)
|
|
||||||
val identificationNumbers = mutableListOf<String>()
|
|
||||||
horse.lebensnummer?.let { identificationNumbers.add("Lebensnummer: $it") }
|
|
||||||
horse.oepsNummer?.let { identificationNumbers.add("OEPS: $it") }
|
|
||||||
|
|
||||||
if (identificationNumbers.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🆔",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = identificationNumbers.joinToString(" | "),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status indicators
|
|
||||||
val statusList = mutableListOf<String>()
|
|
||||||
if (horse.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv")
|
|
||||||
if (horse.isOepsRegistered()) statusList.add("OEPS registriert")
|
|
||||||
if (horse.isFeiRegistered()) statusList.add("FEI registriert")
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Status: ${statusList.joinToString(", ")}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
// Data source
|
|
||||||
Text(
|
|
||||||
text = "Datenquelle: ${horse.datenQuelle.name}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A badge that displays the horse's registration status
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun HorseStatusBadge(horse: DomPferd) {
|
|
||||||
val status = when {
|
|
||||||
horse.isFeiRegistered() -> "FEI"
|
|
||||||
horse.isOepsRegistered() -> "OEPS"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
status?.let {
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-266
@@ -1,266 +0,0 @@
|
|||||||
package at.mocode.client.common.components.masterdata
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
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.masterdata.domain.model.LandDefinition
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compose component that displays master data (Stammdaten).
|
|
||||||
* This is a Compose-based replacement for the React-based StammdatenListe component.
|
|
||||||
* Currently focuses on countries (LandDefinition) but can be extended for other master data types.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun StammdatenListe(
|
|
||||||
countries: List<LandDefinition> = emptyList(),
|
|
||||||
isLoading: Boolean = false,
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onCountryClick: (LandDefinition) -> Unit = {}
|
|
||||||
) {
|
|
||||||
// UI rendering with Compose
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Stammdaten",
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Länder",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
when {
|
|
||||||
isLoading -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errorMessage != null -> {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = errorMessage,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
countries.isEmpty() -> {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Keine Länder verfügbar",
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Adaptive(minSize = 300.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(countries) { country ->
|
|
||||||
CountryCard(country = country, onClick = { onCountryClick(country) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CountryCard(
|
|
||||||
country: LandDefinition,
|
|
||||||
onClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
|
||||||
onClick = onClick
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = country.nameDeutsch,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// ISO codes
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🌍",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "ISO-Codes: ${country.isoAlpha2Code} / ${country.isoAlpha3Code}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
country.isoNumerischerCode?.let { numCode ->
|
|
||||||
Text(
|
|
||||||
text = " / $numCode",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// English name if available
|
|
||||||
country.nameEnglisch?.let { englishName ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🇬🇧",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Englischer Name: $englishName",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EU/EWR membership
|
|
||||||
val membershipInfo = mutableListOf<String>()
|
|
||||||
country.istEuMitglied?.let { isEuMember ->
|
|
||||||
if (isEuMember) membershipInfo.add("EU-Mitglied")
|
|
||||||
}
|
|
||||||
country.istEwrMitglied?.let { isEwrMember ->
|
|
||||||
if (isEwrMember) membershipInfo.add("EWR-Mitglied")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (membershipInfo.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🇪🇺",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Mitgliedschaft: ${membershipInfo.joinToString(", ")}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "ℹ️",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Status: ${if (country.istAktiv) "Aktiv" else "Inaktiv"}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort order if available
|
|
||||||
country.sortierReihenfolge?.let { sortOrder ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🔢",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Sortierreihenfolge: $sortOrder",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Coat of arms/flag URL if available
|
|
||||||
country.wappenUrl?.let { flagUrl ->
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🏴",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Wappen/Flagge: $flagUrl",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A badge that displays the country's EU/EWR membership status
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CountryMembershipBadge(country: LandDefinition) {
|
|
||||||
val membership = when {
|
|
||||||
country.istEuMitglied == true -> "EU"
|
|
||||||
country.istEwrMitglied == true -> "EWR"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
membership?.let {
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package at.mocode.client.common.config
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration class for API client settings.
|
|
||||||
* Allows for environment-specific configuration.
|
|
||||||
*/
|
|
||||||
data class ApiConfig(
|
|
||||||
val baseUrl: String = System.getProperty("api.base.url") ?: System.getenv("API_BASE_URL") ?: "http://localhost:8080",
|
|
||||||
val requestTimeoutMs: Long = System.getProperty("api.timeout")?.toLongOrNull() ?: 30_000L,
|
|
||||||
val cacheTtlMs: Long = System.getProperty("api.cache.ttl")?.toLongOrNull() ?: 30_000L,
|
|
||||||
val maxCacheSize: Int = System.getProperty("api.cache.max.size")?.toIntOrNull() ?: 1000,
|
|
||||||
val enableLogging: Boolean = System.getProperty("api.logging.enabled")?.toBoolean() ?: false
|
|
||||||
)
|
|
||||||
-175
@@ -1,175 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import at.mocode.client.common.api.ApiClient
|
|
||||||
import at.mocode.client.common.api.ApiException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base repository class that provides common CRUD operations for client-side repositories.
|
|
||||||
* Eliminates code duplication and provides consistent error handling across all repositories.
|
|
||||||
*/
|
|
||||||
abstract class BaseClientRepository<T : Any>(
|
|
||||||
protected val baseEndpoint: String
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds an entity by its ID.
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> findEntityById(id: String, entityClass: Class<T>): T? {
|
|
||||||
return try {
|
|
||||||
// Note: This is a simplified version - in practice you'd use reflection or other means
|
|
||||||
// to handle the generic type properly
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
ApiClient.get<Any>("$baseEndpoint/$id") as? T
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to fetch entity with ID $id", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all active entities with pagination.
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> findAllActiveEntities(limit: Int, offset: Int, entityClass: Class<T>): List<T> {
|
|
||||||
return try {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(ApiClient.get<Any>("$baseEndpoint?limit=$limit&offset=$offset") as? List<T>) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to fetch active entities", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches entities by name/search term.
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> searchEntities(searchTerm: String, limit: Int, entityClass: Class<T>): List<T> {
|
|
||||||
return try {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(ApiClient.get<Any>("$baseEndpoint?search=$searchTerm&limit=$limit") as? List<T>) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to search entities by term: $searchTerm", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches entities by a specific field.
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> searchEntitiesByField(
|
|
||||||
fieldName: String,
|
|
||||||
fieldValue: String,
|
|
||||||
limit: Int,
|
|
||||||
entityClass: Class<T>
|
|
||||||
): List<T> {
|
|
||||||
return try {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(ApiClient.get<Any>("$baseEndpoint?$fieldName=$fieldValue&limit=$limit") as? List<T>) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to search entities by $fieldName: $fieldValue", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches entities by date range.
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> searchEntitiesByDateRange(
|
|
||||||
startDate: String,
|
|
||||||
endDate: String,
|
|
||||||
limit: Int,
|
|
||||||
entityClass: Class<T>
|
|
||||||
): List<T> {
|
|
||||||
return try {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(ApiClient.get<Any>("$baseEndpoint?startDate=$startDate&endDate=$endDate&limit=$limit") as? List<T>) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to search entities by date range: $startDate to $endDate", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves an entity (create or update based on ID).
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> saveEntity(entity: T, getId: (T) -> String, entityClass: Class<T>): T {
|
|
||||||
return try {
|
|
||||||
val id = getId(entity)
|
|
||||||
if (id.isBlank()) {
|
|
||||||
// Create new entity
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
ApiClient.post<Any>(baseEndpoint, entity as Any) as T
|
|
||||||
} else {
|
|
||||||
// Update existing entity
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
ApiClient.put<Any>("$baseEndpoint/$id", entity as Any) as T
|
|
||||||
}
|
|
||||||
} catch (e: ApiException) {
|
|
||||||
logError("Failed to save entity", e)
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Unexpected error while saving entity", e)
|
|
||||||
throw ApiException(
|
|
||||||
message = "Failed to save entity: ${e.message}",
|
|
||||||
code = "SAVE_ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an entity by ID.
|
|
||||||
*/
|
|
||||||
protected suspend fun deleteEntity(id: String): Boolean {
|
|
||||||
return try {
|
|
||||||
ApiClient.delete<Boolean>("$baseEndpoint/$id")
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to delete entity with ID $id", e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts active entities.
|
|
||||||
*/
|
|
||||||
protected suspend fun countActiveEntities(): Long {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<Long>("$baseEndpoint/count") ?: 0L
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to count active entities", e)
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets entities from a specific sub-endpoint.
|
|
||||||
*/
|
|
||||||
protected suspend fun <T> getFromSubEndpoint(subEndpoint: String, limit: Int, entityClass: Class<T>): List<T> {
|
|
||||||
return try {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(ApiClient.get<Any>("$baseEndpoint/$subEndpoint?limit=$limit") as? List<T>) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to fetch from sub-endpoint: $subEndpoint", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs errors in a consistent format.
|
|
||||||
* In a real application, this should use a proper logging framework.
|
|
||||||
*/
|
|
||||||
internal fun logError(message: String, exception: Exception) {
|
|
||||||
println("[ERROR] ${this::class.simpleName}: $message - ${exception.message}")
|
|
||||||
// TODO: Replace with proper logging framework (e.g., SLF4J)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates cache entries related to this repository's endpoint.
|
|
||||||
*/
|
|
||||||
protected fun invalidateCache() {
|
|
||||||
// Extract base path for cache invalidation
|
|
||||||
val basePath = baseEndpoint.split("/").take(3).joinToString("/")
|
|
||||||
// For now, clear all cache - in a real implementation, we'd use pattern matching
|
|
||||||
ApiClient.clearCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import at.mocode.client.common.api.ApiClient
|
|
||||||
import at.mocode.client.common.api.ApiException
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side implementation of the EventRepository interface.
|
|
||||||
* Uses the ApiClient to make HTTP requests to the backend API.
|
|
||||||
*/
|
|
||||||
class ClientEventRepository : EventRepository {
|
|
||||||
|
|
||||||
private val baseEndpoint = "/api/events"
|
|
||||||
|
|
||||||
override suspend fun findById(id: String): Event? {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<Event>("$baseEndpoint/$id")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to fetch event with ID $id: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Event> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Event>>("$baseEndpoint?limit=$limit&offset=$offset") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to fetch active events: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Event> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Event>>("$baseEndpoint?search=$searchTerm&limit=$limit") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to search events by name: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByLocation(location: String, limit: Int): List<Event> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Event>>("$baseEndpoint?location=$location&limit=$limit") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to search events by location: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, limit: Int): List<Event> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Event>>("$baseEndpoint?startDate=$startDate&endDate=$endDate&limit=$limit") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to search events by date range: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findUpcoming(limit: Int): List<Event> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Event>>("$baseEndpoint/upcoming?limit=$limit") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to fetch upcoming events: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(event: Event): Event {
|
|
||||||
return try {
|
|
||||||
if (event.id.isBlank()) {
|
|
||||||
// Create new event
|
|
||||||
ApiClient.post<Event>(baseEndpoint, event)
|
|
||||||
} else {
|
|
||||||
// Update existing event
|
|
||||||
ApiClient.put<Event>("$baseEndpoint/${event.id}", event)
|
|
||||||
}
|
|
||||||
} catch (e: ApiException) {
|
|
||||||
println("[ERROR] Failed to save event: ${e.message}")
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Unexpected error while saving event: ${e.message}")
|
|
||||||
throw ApiException(
|
|
||||||
message = "Failed to save event: ${e.message}",
|
|
||||||
code = "SAVE_ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: String): Boolean {
|
|
||||||
return try {
|
|
||||||
ApiClient.delete<Boolean>("$baseEndpoint/$id")
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to delete event with ID $id: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countActive(): Long {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<Long>("$baseEndpoint/count") ?: 0L
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to count active events: ${e.message}")
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import at.mocode.client.common.api.ApiClient
|
|
||||||
import at.mocode.client.common.api.ApiException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side implementation of the PersonRepository interface.
|
|
||||||
* Uses the ApiClient to make HTTP requests to the backend API.
|
|
||||||
*/
|
|
||||||
class ClientPersonRepository : PersonRepository {
|
|
||||||
|
|
||||||
private val baseEndpoint = "/api/persons"
|
|
||||||
|
|
||||||
override suspend fun findById(id: String): Person? {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<Person>("$baseEndpoint/$id")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to fetch person with ID $id: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Person> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Person>>("$baseEndpoint?limit=$limit&offset=$offset") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to fetch active persons: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Person> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Person>>("$baseEndpoint?search=$searchTerm&limit=$limit") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to search persons by name: ${e.message}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(person: Person): Person {
|
|
||||||
return try {
|
|
||||||
if (person.id.isBlank()) {
|
|
||||||
// Create new person
|
|
||||||
ApiClient.post<Person>(baseEndpoint, person)
|
|
||||||
} else {
|
|
||||||
// Update existing person
|
|
||||||
ApiClient.put<Person>("$baseEndpoint/${person.id}", person)
|
|
||||||
}
|
|
||||||
} catch (e: ApiException) {
|
|
||||||
println("[ERROR] Failed to save person: ${e.message}")
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Unexpected error while saving person: ${e.message}")
|
|
||||||
throw ApiException(
|
|
||||||
message = "Failed to save person: ${e.message}",
|
|
||||||
code = "SAVE_ERROR",
|
|
||||||
details = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: String): Boolean {
|
|
||||||
return try {
|
|
||||||
ApiClient.delete<Boolean>("$baseEndpoint/$id")
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to delete person with ID $id: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countActive(): Long {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<Long>("$baseEndpoint/count") ?: 0L
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[ERROR] Failed to count active persons: ${e.message}")
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simplified Event data class for client-side use.
|
|
||||||
* This is a client-side representation of the Veranstaltung entity from the domain model.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class Event(
|
|
||||||
val id: String = "",
|
|
||||||
val name: String,
|
|
||||||
val beschreibung: String? = null,
|
|
||||||
val startDatum: LocalDate,
|
|
||||||
val endDatum: LocalDate,
|
|
||||||
val ort: String,
|
|
||||||
val veranstalterVereinId: String? = null,
|
|
||||||
val sparten: List<String> = emptyList(),
|
|
||||||
val istAktiv: Boolean = true,
|
|
||||||
val istOeffentlich: Boolean = true,
|
|
||||||
val maxTeilnehmer: Int? = null,
|
|
||||||
val anmeldeschluss: LocalDate? = null,
|
|
||||||
val createdAt: String? = null,
|
|
||||||
val updatedAt: String? = null
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Checks if the event is currently accepting registrations.
|
|
||||||
*/
|
|
||||||
fun isRegistrationOpen(): Boolean {
|
|
||||||
// Simplified implementation - can be enhanced with proper date comparison
|
|
||||||
return istAktiv && anmeldeschluss != null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the duration of the event in days.
|
|
||||||
*/
|
|
||||||
fun getDurationInDays(): Int {
|
|
||||||
return (endDatum.toEpochDays() - startDatum.toEpochDays()).toInt() + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the event spans multiple days.
|
|
||||||
*/
|
|
||||||
fun isMultiDay(): Boolean {
|
|
||||||
return startDatum != endDatum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side repository interface for Event entities.
|
|
||||||
* This is a simplified version of the domain repository interface.
|
|
||||||
*/
|
|
||||||
interface EventRepository {
|
|
||||||
/**
|
|
||||||
* Finds an event by its ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the event
|
|
||||||
* @return The event if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findById(id: String): Event?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all active events with pagination.
|
|
||||||
*
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @param offset Number of results to skip
|
|
||||||
* @return List of active events
|
|
||||||
*/
|
|
||||||
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<Event>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds events by name (partial match).
|
|
||||||
*
|
|
||||||
* @param searchTerm The search term to match against event names
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of matching events
|
|
||||||
*/
|
|
||||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<Event>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds events by location (partial match).
|
|
||||||
*
|
|
||||||
* @param location The location to match against event locations
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of matching events
|
|
||||||
*/
|
|
||||||
suspend fun findByLocation(location: String, limit: Int = 50): List<Event>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds events by date range.
|
|
||||||
*
|
|
||||||
* @param startDate The start date of the range
|
|
||||||
* @param endDate The end date of the range
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of events within the date range
|
|
||||||
*/
|
|
||||||
suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, limit: Int = 100): List<Event>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds upcoming events.
|
|
||||||
*
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of upcoming events
|
|
||||||
*/
|
|
||||||
suspend fun findUpcoming(limit: Int = 50): List<Event>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves an event (create or update).
|
|
||||||
*
|
|
||||||
* @param event The event to save
|
|
||||||
* @return The saved event with updated information
|
|
||||||
*/
|
|
||||||
suspend fun save(event: Event): Event
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an event by ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the event to delete
|
|
||||||
* @return true if the event was deleted, false if not found
|
|
||||||
*/
|
|
||||||
suspend fun delete(id: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of active events.
|
|
||||||
*
|
|
||||||
* @return The total count of active events
|
|
||||||
*/
|
|
||||||
suspend fun countActive(): Long
|
|
||||||
}
|
|
||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import at.mocode.client.common.api.ApiClient
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimized client-side implementation of the PersonRepository interface.
|
|
||||||
* Uses BaseClientRepository to eliminate code duplication and provide consistent error handling.
|
|
||||||
*/
|
|
||||||
class OptimizedClientPersonRepository : BaseClientRepository<Person>("/api/persons"), PersonRepository {
|
|
||||||
|
|
||||||
override suspend fun findById(id: String): Person? {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<Person>("$baseEndpoint/$id")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to fetch person with ID $id", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Person> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Person>>("$baseEndpoint?limit=$limit&offset=$offset") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to fetch active persons", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Person> {
|
|
||||||
return try {
|
|
||||||
ApiClient.get<List<Person>>("$baseEndpoint?search=$searchTerm&limit=$limit") ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to search persons by name", e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(person: Person): Person {
|
|
||||||
return try {
|
|
||||||
val result = if (person.id.isBlank()) {
|
|
||||||
// Create new person
|
|
||||||
ApiClient.post<Person>(baseEndpoint, person)
|
|
||||||
} else {
|
|
||||||
// Update existing person
|
|
||||||
ApiClient.put<Person>("$baseEndpoint/${person.id}", person)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate related cache entries after successful save
|
|
||||||
invalidateCache()
|
|
||||||
|
|
||||||
result
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to save person", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: String): Boolean {
|
|
||||||
return try {
|
|
||||||
ApiClient.delete<Boolean>("$baseEndpoint/$id")
|
|
||||||
|
|
||||||
// Invalidate related cache entries after successful delete
|
|
||||||
invalidateCache()
|
|
||||||
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError("Failed to delete person with ID $id", e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countActive(): Long {
|
|
||||||
return countActiveEntities()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simplified Person data class for client-side use.
|
|
||||||
* This is a client-side representation of the DomPerson entity from the domain model.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class Person(
|
|
||||||
val id: String = "",
|
|
||||||
val nachname: String,
|
|
||||||
val vorname: String,
|
|
||||||
val titel: String? = null,
|
|
||||||
val oepsSatzNr: String? = null,
|
|
||||||
val geburtsdatum: LocalDate? = null,
|
|
||||||
val geschlecht: String? = null,
|
|
||||||
val telefon: String? = null,
|
|
||||||
val email: String? = null,
|
|
||||||
val strasse: String? = null,
|
|
||||||
val plz: String? = null,
|
|
||||||
val ort: String? = null,
|
|
||||||
val adresszusatz: String? = null,
|
|
||||||
val feiId: String? = null,
|
|
||||||
val mitgliedsNummer: String? = null,
|
|
||||||
val istGesperrt: Boolean = false,
|
|
||||||
val sperrGrund: String? = null,
|
|
||||||
val notizen: String? = null,
|
|
||||||
val datenQuelle: String = "MANUELL",
|
|
||||||
val createdAt: String? = null,
|
|
||||||
val updatedAt: String? = null
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Returns the full name of the person, including title if available.
|
|
||||||
*/
|
|
||||||
fun getFullName(): String {
|
|
||||||
return buildString {
|
|
||||||
titel?.let { append("$it ") }
|
|
||||||
append("$vorname $nachname")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a display-friendly representation of the address.
|
|
||||||
*/
|
|
||||||
fun getFormattedAddress(): String? {
|
|
||||||
if (strasse == null || plz == null || ort == null) return null
|
|
||||||
|
|
||||||
return buildString {
|
|
||||||
append(strasse)
|
|
||||||
adresszusatz?.let { append(", $it") }
|
|
||||||
append(", $plz $ort")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-56
@@ -1,56 +0,0 @@
|
|||||||
package at.mocode.client.common.repository
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side repository interface for Person entities.
|
|
||||||
* This is a simplified version of the domain repository interface.
|
|
||||||
*/
|
|
||||||
interface PersonRepository {
|
|
||||||
/**
|
|
||||||
* Finds a person by their ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the person
|
|
||||||
* @return The person if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findById(id: String): Person?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all active persons with pagination.
|
|
||||||
*
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @param offset Number of results to skip
|
|
||||||
* @return List of active persons
|
|
||||||
*/
|
|
||||||
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<Person>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds persons by name (partial match).
|
|
||||||
*
|
|
||||||
* @param searchTerm The search term to match against person names
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of matching persons
|
|
||||||
*/
|
|
||||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<Person>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a person (create or update).
|
|
||||||
*
|
|
||||||
* @param person The person to save
|
|
||||||
* @return The saved person with updated information
|
|
||||||
*/
|
|
||||||
suspend fun save(person: Person): Person
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a person by ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the person to delete
|
|
||||||
* @return true if the person was deleted, false if not found
|
|
||||||
*/
|
|
||||||
suspend fun delete(id: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of active persons.
|
|
||||||
*
|
|
||||||
* @return The total count of active persons
|
|
||||||
*/
|
|
||||||
suspend fun countActive(): Long
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package at.mocode.client.common.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,31 @@
|
|||||||
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
|
||||||
alias(libs.plugins.compose.multiplatform)
|
alias(libs.plugins.compose.multiplatform)
|
||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
kotlin {
|
||||||
// Greift explizit auf den "desktop" (JVM) Teil unseres KMP-Moduls zu.
|
jvm("desktop")
|
||||||
implementation(projects.client.commonUi)
|
|
||||||
|
|
||||||
// Stellt die Desktop-spezifischen Teile von Jetpack Compose bereit.
|
sourceSets {
|
||||||
implementation(compose.desktop.currentOs)
|
val desktopMain by getting {
|
||||||
|
dependencies {
|
||||||
// Stellt die Coroutine-Integration für die Swing-UI-Bibliothek bereit.
|
implementation(libs.compose.desktop.currentOs)
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(project(":client:common-ui"))
|
||||||
|
}
|
||||||
// --- Testing ---
|
}
|
||||||
testImplementation(projects.platform.platformTesting)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "at.mocode.client.desktop.MainKt"
|
mainClass = "MainKt"
|
||||||
|
nativeDistributions {
|
||||||
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
|
packageName = "MeldestellePro"
|
||||||
|
packageVersion = "1.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import at.mocode.client.ui.App
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
Window(onCloseRequest = ::exitApplication, title = "MeldestellePro") {
|
||||||
|
// Wir rufen hier exakt dieselbe geteilte App() Composable-Funktion auf.
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,469 +0,0 @@
|
|||||||
package at.mocode.client.desktop
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import at.mocode.client.common.BaseApp
|
|
||||||
import at.mocode.client.common.components.events.VeranstaltungsListe
|
|
||||||
import at.mocode.client.common.components.horses.PferdeListe
|
|
||||||
import at.mocode.client.common.components.masterdata.StammdatenListe
|
|
||||||
import at.mocode.client.web.screens.CreatePersonScreen
|
|
||||||
import at.mocode.client.web.screens.PersonListScreen
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.events.domain.model.Veranstaltung
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.masterdata.domain.model.LandDefinition
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.engine.cio.*
|
|
||||||
import io.ktor.client.plugins.auth.*
|
|
||||||
import io.ktor.client.plugins.auth.providers.*
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main application composable for the desktop application.
|
|
||||||
* Implements a simple tab-based navigation between different screens.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun App() {
|
|
||||||
// State for navigation
|
|
||||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
|
||||||
|
|
||||||
// Define tabs
|
|
||||||
val tabs = listOf(
|
|
||||||
TabItem("Dashboard", Icons.Default.Home),
|
|
||||||
TabItem("Veranstaltungen", Icons.Default.Event),
|
|
||||||
TabItem("Pferde", Icons.Default.Pets),
|
|
||||||
TabItem("Personen", Icons.Default.Person),
|
|
||||||
TabItem("Stammdaten", Icons.Default.Settings)
|
|
||||||
)
|
|
||||||
|
|
||||||
BaseApp {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text("Meldestelle - Reitersport Management") }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
// Tab row for navigation
|
|
||||||
TabRow(
|
|
||||||
selectedTabIndex = selectedTabIndex
|
|
||||||
) {
|
|
||||||
tabs.forEachIndexed { index, tab ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedTabIndex == index,
|
|
||||||
onClick = { selectedTabIndex = index },
|
|
||||||
text = { Text(tab.title) },
|
|
||||||
icon = { Icon(tab.icon, contentDescription = tab.title) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content based on selected tab
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
when (selectedTabIndex) {
|
|
||||||
0 -> DashboardScreen()
|
|
||||||
1 -> EventsScreen()
|
|
||||||
2 -> HorsesScreen()
|
|
||||||
3 -> PersonsScreen()
|
|
||||||
4 -> MasterDataScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class representing a tab item
|
|
||||||
*/
|
|
||||||
data class TabItem(
|
|
||||||
val title: String,
|
|
||||||
val icon: androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dashboard screen showing an overview of the application
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun DashboardScreen() {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var pingResult by remember { mutableStateOf<String?>(null) }
|
|
||||||
var pingLoading by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Willkommen bei Meldestelle",
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Reitersport Management System",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.padding(bottom = 32.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Display ping result if available
|
|
||||||
pingResult?.let { result ->
|
|
||||||
Text(
|
|
||||||
text = "Ping Result: $result",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick access buttons
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = { /* TODO: Implement quick action */ }
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = "Neue Veranstaltung")
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text("Neue Veranstaltung")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = { /* TODO: Implement quick action */ }
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Icon(Icons.Default.Search, contentDescription = "Suche")
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text("Suche")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
pingLoading = true
|
|
||||||
try {
|
|
||||||
val pingClient = HttpClient(CIO) {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(Json { ignoreUnknownKeys = true })
|
|
||||||
}
|
|
||||||
install(Auth) {
|
|
||||||
basic {
|
|
||||||
credentials {
|
|
||||||
BasicAuthCredentials(username = "admin", password = "admin")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val response: Map<String, String> = pingClient.get("http://localhost:8080/api/ping").body()
|
|
||||||
pingResult = response["status"] ?: "No status in response"
|
|
||||||
|
|
||||||
pingClient.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
pingResult = "Error: ${e.message}"
|
|
||||||
} finally {
|
|
||||||
pingLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !pingLoading
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Icon(
|
|
||||||
if (pingLoading) Icons.Default.Refresh else Icons.Default.NetworkCheck,
|
|
||||||
contentDescription = "Ping Test"
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(if (pingLoading) "Pinging..." else "Ping Test")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Events screen showing a list of events
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun EventsScreen() {
|
|
||||||
// Create some dummy event data for testing
|
|
||||||
val dummyEvents = remember {
|
|
||||||
listOf(
|
|
||||||
Veranstaltung(
|
|
||||||
name = "Reitturnier Wien",
|
|
||||||
ort = "Wien",
|
|
||||||
startDatum = LocalDate(2025, 8, 15),
|
|
||||||
endDatum = LocalDate(2025, 8, 17),
|
|
||||||
veranstalterVereinId = com.benasher44.uuid.uuid4(),
|
|
||||||
beschreibung = "Internationales Reitturnier mit Springprüfungen",
|
|
||||||
istAktiv = true,
|
|
||||||
istOeffentlich = true,
|
|
||||||
anmeldeschluss = LocalDate(2025, 8, 1),
|
|
||||||
maxTeilnehmer = 100
|
|
||||||
),
|
|
||||||
Veranstaltung(
|
|
||||||
name = "Dressurturnier Salzburg",
|
|
||||||
ort = "Salzburg",
|
|
||||||
startDatum = LocalDate(2025, 9, 5),
|
|
||||||
endDatum = LocalDate(2025, 9, 5),
|
|
||||||
veranstalterVereinId = com.benasher44.uuid.uuid4(),
|
|
||||||
beschreibung = "Dressurturnier für alle Altersklassen",
|
|
||||||
istAktiv = true,
|
|
||||||
istOeffentlich = true,
|
|
||||||
anmeldeschluss = LocalDate(2025, 8, 25),
|
|
||||||
maxTeilnehmer = 50
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the VeranstaltungsListe component to display the events
|
|
||||||
VeranstaltungsListe(
|
|
||||||
events = dummyEvents,
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Horses screen showing a list of horses
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun HorsesScreen() {
|
|
||||||
// Create some dummy horse data for testing
|
|
||||||
val dummyHorses = remember {
|
|
||||||
listOf(
|
|
||||||
DomPferd(
|
|
||||||
pferdeName = "Maestoso Bella",
|
|
||||||
geschlecht = PferdeGeschlechtE.STUTE,
|
|
||||||
geburtsdatum = LocalDate(2018, 5, 12),
|
|
||||||
rasse = "Lipizzaner",
|
|
||||||
farbe = "Schimmel",
|
|
||||||
lebensnummer = "AT2018123456",
|
|
||||||
chipNummer = "276098100123456",
|
|
||||||
oepsNummer = "AT12345",
|
|
||||||
stockmass = 165,
|
|
||||||
istAktiv = true,
|
|
||||||
datenQuelle = DatenQuelleE.MANUELL
|
|
||||||
),
|
|
||||||
DomPferd(
|
|
||||||
pferdeName = "Donnerhall",
|
|
||||||
geschlecht = PferdeGeschlechtE.HENGST,
|
|
||||||
geburtsdatum = LocalDate(2020, 3, 24),
|
|
||||||
rasse = "Hannoveraner",
|
|
||||||
farbe = "Rappe",
|
|
||||||
lebensnummer = "DE2020654321",
|
|
||||||
passNummer = "DE98765",
|
|
||||||
feiNummer = "FEI10293847",
|
|
||||||
vaterName = "Dressage King",
|
|
||||||
mutterName = "Hannelore",
|
|
||||||
stockmass = 172,
|
|
||||||
istAktiv = true,
|
|
||||||
datenQuelle = DatenQuelleE.MANUELL
|
|
||||||
),
|
|
||||||
DomPferd(
|
|
||||||
pferdeName = "Lucky Star",
|
|
||||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
|
||||||
geburtsdatum = LocalDate(2015, 7, 8),
|
|
||||||
rasse = "Haflinger",
|
|
||||||
farbe = "Fuchs",
|
|
||||||
chipNummer = "276098100654321",
|
|
||||||
istAktiv = true,
|
|
||||||
datenQuelle = DatenQuelleE.MANUELL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the PferdeListe component to display the horses
|
|
||||||
PferdeListe(
|
|
||||||
horses = dummyHorses,
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = null,
|
|
||||||
onHorseClick = { /* Handle horse click */ }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persons screen showing a list of persons
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun PersonsScreen() {
|
|
||||||
// State for navigation
|
|
||||||
var showCreatePerson by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// Create view models using AppDependencies
|
|
||||||
val personListViewModel = remember { at.mocode.client.web.di.AppDependencies.personListViewModel() }
|
|
||||||
val createPersonViewModel = remember { at.mocode.client.web.di.AppDependencies.createPersonViewModel() }
|
|
||||||
|
|
||||||
if (showCreatePerson) {
|
|
||||||
// Show create person screen
|
|
||||||
CreatePersonScreen(
|
|
||||||
viewModel = createPersonViewModel,
|
|
||||||
onNavigateBack = {
|
|
||||||
// When navigating back, refresh the person list if a person was created
|
|
||||||
if (createPersonViewModel.isSuccess) {
|
|
||||||
personListViewModel.refreshPersons()
|
|
||||||
}
|
|
||||||
showCreatePerson = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Show person list screen
|
|
||||||
PersonListScreen(
|
|
||||||
viewModel = personListViewModel,
|
|
||||||
onNavigateToCreatePerson = { showCreatePerson = true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Master data screen showing master data like countries
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun MasterDataScreen() {
|
|
||||||
// Create some dummy country data for testing
|
|
||||||
val dummyCountries = remember {
|
|
||||||
listOf(
|
|
||||||
LandDefinition(
|
|
||||||
isoAlpha2Code = "AT",
|
|
||||||
isoAlpha3Code = "AUT",
|
|
||||||
isoNumerischerCode = "040",
|
|
||||||
nameDeutsch = "Österreich",
|
|
||||||
nameEnglisch = "Austria",
|
|
||||||
istEuMitglied = true,
|
|
||||||
istEwrMitglied = true,
|
|
||||||
istAktiv = true,
|
|
||||||
sortierReihenfolge = 1
|
|
||||||
),
|
|
||||||
LandDefinition(
|
|
||||||
isoAlpha2Code = "DE",
|
|
||||||
isoAlpha3Code = "DEU",
|
|
||||||
isoNumerischerCode = "276",
|
|
||||||
nameDeutsch = "Deutschland",
|
|
||||||
nameEnglisch = "Germany",
|
|
||||||
istEuMitglied = true,
|
|
||||||
istEwrMitglied = true,
|
|
||||||
istAktiv = true,
|
|
||||||
sortierReihenfolge = 2
|
|
||||||
),
|
|
||||||
LandDefinition(
|
|
||||||
isoAlpha2Code = "CH",
|
|
||||||
isoAlpha3Code = "CHE",
|
|
||||||
isoNumerischerCode = "756",
|
|
||||||
nameDeutsch = "Schweiz",
|
|
||||||
nameEnglisch = "Switzerland",
|
|
||||||
istEuMitglied = false,
|
|
||||||
istEwrMitglied = false,
|
|
||||||
istAktiv = true,
|
|
||||||
sortierReihenfolge = 3
|
|
||||||
),
|
|
||||||
LandDefinition(
|
|
||||||
isoAlpha2Code = "IT",
|
|
||||||
isoAlpha3Code = "ITA",
|
|
||||||
isoNumerischerCode = "380",
|
|
||||||
nameDeutsch = "Italien",
|
|
||||||
nameEnglisch = "Italy",
|
|
||||||
istEuMitglied = true,
|
|
||||||
istEwrMitglied = true,
|
|
||||||
istAktiv = true,
|
|
||||||
sortierReihenfolge = 4
|
|
||||||
),
|
|
||||||
LandDefinition(
|
|
||||||
isoAlpha2Code = "FR",
|
|
||||||
isoAlpha3Code = "FRA",
|
|
||||||
isoNumerischerCode = "250",
|
|
||||||
nameDeutsch = "Frankreich",
|
|
||||||
nameEnglisch = "France",
|
|
||||||
istEuMitglied = true,
|
|
||||||
istEwrMitglied = true,
|
|
||||||
istAktiv = true,
|
|
||||||
sortierReihenfolge = 5
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the StammdatenListe component to display the countries
|
|
||||||
StammdatenListe(
|
|
||||||
countries = dummyCountries,
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = null,
|
|
||||||
onCountryClick = { /* Handle country click */ }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic placeholder screen
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun PlaceholderScreen(
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
contentDescription = title,
|
|
||||||
modifier = Modifier.size(64.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(horizontal = 32.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = { /* TODO: Implement action */ }
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = "Hinzufügen")
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Hinzufügen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package at.mocode.client.desktop
|
|
||||||
|
|
||||||
import androidx.compose.ui.window.Window
|
|
||||||
import androidx.compose.ui.window.application
|
|
||||||
|
|
||||||
fun main() = application {
|
|
||||||
Window(
|
|
||||||
onCloseRequest = ::exitApplication,
|
|
||||||
title = "Meldestelle - Reitersport Management"
|
|
||||||
) {
|
|
||||||
App()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,29 @@ plugins {
|
|||||||
kotlin {
|
kotlin {
|
||||||
js(IR) {
|
js(IR) {
|
||||||
browser {
|
browser {
|
||||||
// Konfiguriert den Development-Server und die finalen Bundles.
|
|
||||||
commonWebpackConfig {
|
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()
|
binaries.executable()
|
||||||
@@ -18,23 +38,9 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Greift explizit auf den JS-Teil unseres KMP-Moduls zu.
|
implementation(project(":client:common-ui"))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -56,6 +56,7 @@ reactorKafka = "1.3.22"
|
|||||||
jackson = "2.17.0"
|
jackson = "2.17.0"
|
||||||
jakartaAnnotation = "2.1.1"
|
jakartaAnnotation = "2.1.1"
|
||||||
roomCommonJvm = "2.7.2"
|
roomCommonJvm = "2.7.2"
|
||||||
|
uiDesktop = "1.7.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# --- Platform BOMs (Bill of Materials) ---
|
# --- Platform BOMs (Bill of Materials) ---
|
||||||
@@ -178,6 +179,7 @@ testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.
|
|||||||
testcontainers-kafka = { module = "org.testcontainers:kafka", version.ref = "testcontainers" }
|
testcontainers-kafka = { module = "org.testcontainers:kafka", version.ref = "testcontainers" }
|
||||||
reactor-test = { module = "io.projectreactor:reactor-test" } # Version wird von der Spring BOM verwaltet
|
reactor-test = { module = "io.projectreactor:reactor-test" } # Version wird von der Spring BOM verwaltet
|
||||||
room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "roomCommonJvm" }
|
room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "roomCommonJvm" }
|
||||||
|
ui-desktop = { group = "androidx.compose.ui", name = "ui-desktop", version.ref = "uiDesktop" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
# OPTIMIERUNG: Bündelt gängige Abhängigkeitsgruppen.
|
# OPTIMIERUNG: Bündelt gängige Abhängigkeitsgruppen.
|
||||||
|
|||||||
+3
-6
@@ -14,6 +14,9 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||||
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
|
|
||||||
@@ -85,11 +88,5 @@ include(":masterdata:masterdata-infrastructure")
|
|||||||
include(":masterdata:masterdata-api")
|
include(":masterdata:masterdata-api")
|
||||||
include(":masterdata:masterdata-service")
|
include(":masterdata:masterdata-service")
|
||||||
|
|
||||||
// Client modules
|
|
||||||
include(":client:common-ui")
|
|
||||||
include(":client:web-app")
|
|
||||||
include(":client:desktop-app")
|
|
||||||
|
|
||||||
// Legacy modules have been removed after successful migration
|
// Legacy modules have been removed after successful migration
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user