From 23b6708197165bb7d97e4a6f72df796e6529e788 Mon Sep 17 00:00:00 2001 From: stefan Date: Tue, 12 Aug 2025 17:36:55 +0200 Subject: [PATCH] fixing(client-module) --- client/common-ui/build.gradle.kts | 36 +- .../mocode/client/data/api/PingApiClient.kt | 29 ++ .../mocode/client/data/model/PingResponse.kt | 8 + .../kotlin/at/mocode/client/ui/App.kt | 12 + .../client/ui/features/ping/PingScreen.kt | 35 ++ .../client/ui/features/ping/PingViewModel.kt | 24 + .../kotlin/at/mocode/client/common/App.kt | 25 - .../at/mocode/client/common/api/ApiClient.kt | 232 --------- .../at/mocode/client/common/cache/ApiCache.kt | 147 ------ .../components/events/EventComponent.kt | 142 ------ .../components/events/VeranstaltungsListe.kt | 233 --------- .../common/components/horses/PferdeListe.kt | 237 --------- .../components/masterdata/StammdatenListe.kt | 266 ---------- .../mocode/client/common/config/ApiConfig.kt | 13 - .../common/repository/BaseClientRepository.kt | 175 ------- .../repository/ClientEventRepository.kt | 109 ---- .../repository/ClientPersonRepository.kt | 81 --- .../mocode/client/common/repository/Event.kt | 48 -- .../common/repository/EventRepository.kt | 85 ---- .../OptimizedClientPersonRepository.kt | 75 --- .../mocode/client/common/repository/Person.kt | 56 --- .../common/repository/PersonRepository.kt | 56 --- .../at/mocode/client/common/theme/Theme.kt | 49 -- client/desktop-app/build.gradle.kts | 32 +- client/desktop-app/src/main/kotlin/Main.kt | 10 + .../kotlin/at/mocode/client/desktop/App.kt | 469 ------------------ .../kotlin/at/mocode/client/desktop/main.kt | 13 - client/web-app/build.gradle.kts | 42 +- client/web-app/src/jsMain/kotlin/Main.kt | 11 + .../web-app/src/jsMain/resources/index.html | 12 + .../main/kotlin/at/mocode/client/web/App.kt | 174 ------- .../mocode/client/web/di/AppDependencies.kt | 35 -- .../main/kotlin/at/mocode/client/web/main.kt | 10 - .../client/web/screens/CreatePersonScreen.kt | 275 ---------- .../client/web/screens/PersonListScreen.kt | 166 ------- .../web/viewmodel/CreatePersonViewModel.kt | 181 ------- .../web/viewmodel/PersonListViewModel.kt | 86 ---- .../src/main/resources/MeldestelleWebApp.js | 0 client/web-app/src/main/resources/index.html | 19 - .../viewmodel/CreatePersonViewModelTest.kt | 258 ---------- gradle/libs.versions.toml | 2 + settings.gradle.kts | 9 +- 42 files changed, 198 insertions(+), 3779 deletions(-) create mode 100644 client/common-ui/src/commonMain/kotlin/at/mocode/client/data/api/PingApiClient.kt create mode 100644 client/common-ui/src/commonMain/kotlin/at/mocode/client/data/model/PingResponse.kt create mode 100644 client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/App.kt create mode 100644 client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingScreen.kt create mode 100644 client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingViewModel.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/App.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/api/ApiClient.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/cache/ApiCache.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/EventComponent.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/VeranstaltungsListe.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/components/horses/PferdeListe.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/components/masterdata/StammdatenListe.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/config/ApiConfig.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/BaseClientRepository.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientEventRepository.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientPersonRepository.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Event.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/EventRepository.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/OptimizedClientPersonRepository.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Person.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/repository/PersonRepository.kt delete mode 100644 client/common-ui/src/main/kotlin/at/mocode/client/common/theme/Theme.kt create mode 100644 client/desktop-app/src/main/kotlin/Main.kt delete mode 100644 client/desktop-app/src/main/kotlin/at/mocode/client/desktop/App.kt delete mode 100644 client/desktop-app/src/main/kotlin/at/mocode/client/desktop/main.kt create mode 100644 client/web-app/src/jsMain/kotlin/Main.kt create mode 100644 client/web-app/src/jsMain/resources/index.html delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/App.kt delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/di/AppDependencies.kt delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/main.kt delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/screens/CreatePersonScreen.kt delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/screens/PersonListScreen.kt delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModel.kt delete mode 100644 client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/PersonListViewModel.kt delete mode 100644 client/web-app/src/main/resources/MeldestelleWebApp.js delete mode 100644 client/web-app/src/main/resources/index.html delete mode 100644 client/web-app/src/test/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModelTest.kt diff --git a/client/common-ui/build.gradle.kts b/client/common-ui/build.gradle.kts index cfa726ab..0cc00504 100644 --- a/client/common-ui/build.gradle.kts +++ b/client/common-ui/build.gradle.kts @@ -1,63 +1,45 @@ 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.compiler) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.multiplatform) } kotlin { - // Wir definieren die Zielplattformen, für die dieses Modul Code bereitstellt. - jvm("desktop") // Ein JVM-Target für unsere Desktop-App - js(IR) { // Ein JavaScript-Target für unsere Web-App + jvm("desktop") + js(IR) { browser() - binaries.executable() } - // Hier definieren wir die Abhängigkeiten für die jeweiligen Source Sets. sourceSets { val commonMain by getting { dependencies { - // --- Interne Module (für alle Plattformen verfügbar) --- - api(projects.core.coreDomain) - api(projects.core.coreUtils) - - // --- Jetpack Compose UI (für alle Plattformen verfügbar) --- + // Compose UI api(compose.runtime) api(compose.foundation) api(compose.material3) - api(compose.ui) - api(compose.components.resources) 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.contentNegotiation) implementation(libs.ktor.client.serialization.kotlinx.json) - // --- Coroutines (für alle Plattformen verfügbar) --- + // Coroutines for background tasks implementation(libs.kotlinx.coroutines.core) } } - val desktopMain by getting { dependencies { - // Ktor-Engine, die nur für die Desktop (JVM) Version benötigt wird + // Ktor engine for Desktop implementation(libs.ktor.client.cio) } } - val jsMain by getting { dependencies { - // Ktor-Engine, die nur für die Web (JS) Version benötigt wird + // Ktor engine for Browser implementation(libs.ktor.client.js) } } - - val commonTest by getting { - dependencies { - implementation(libs.kotlin.test) - } - } } } diff --git a/client/common-ui/src/commonMain/kotlin/at/mocode/client/data/api/PingApiClient.kt b/client/common-ui/src/commonMain/kotlin/at/mocode/client/data/api/PingApiClient.kt new file mode 100644 index 00000000..d19b493f --- /dev/null +++ b/client/common-ui/src/commonMain/kotlin/at/mocode/client/data/api/PingApiClient.kt @@ -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() + response.status + } catch (e: Exception) { + "Fehler: ${e.message}" + } + } +} diff --git a/client/common-ui/src/commonMain/kotlin/at/mocode/client/data/model/PingResponse.kt b/client/common-ui/src/commonMain/kotlin/at/mocode/client/data/model/PingResponse.kt new file mode 100644 index 00000000..4e29e16d --- /dev/null +++ b/client/common-ui/src/commonMain/kotlin/at/mocode/client/data/model/PingResponse.kt @@ -0,0 +1,8 @@ +package at.mocode.client.data.model + +import kotlinx.serialization.Serializable + +@Serializable +data class PingResponse( + val status: String +) diff --git a/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/App.kt b/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/App.kt new file mode 100644 index 00000000..83099d83 --- /dev/null +++ b/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/App.kt @@ -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() + } +} diff --git a/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingScreen.kt b/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingScreen.kt new file mode 100644 index 00000000..fb245d15 --- /dev/null +++ b/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingScreen.kt @@ -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) + ) + } +} diff --git a/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingViewModel.kt b/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingViewModel.kt new file mode 100644 index 00000000..b2d3bcd3 --- /dev/null +++ b/client/common-ui/src/commonMain/kotlin/at/mocode/client/ui/features/ping/PingViewModel.kt @@ -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" + } + } +} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/App.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/App.kt deleted file mode 100644 index e281f6ee..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/App.kt +++ /dev/null @@ -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() - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/api/ApiClient.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/api/ApiClient.kt deleted file mode 100644 index 84abb162..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/api/ApiClient.kt +++ /dev/null @@ -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>() - 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 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>(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 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>(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 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>(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 delete(endpoint: String): T { - try { - // Make HTTP request - val response = httpClient.delete("$BASE_URL$endpoint") - - val responseText = response.bodyAsText() - val apiResponse = json.decodeFromString>(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? -) : Exception(message) diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/cache/ApiCache.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/cache/ApiCache.kt deleted file mode 100644 index 8d8614fd..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/cache/ApiCache.kt +++ /dev/null @@ -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() - private val accessOrder = ConcurrentLinkedQueue() - 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 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 - ) -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/EventComponent.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/EventComponent.kt deleted file mode 100644 index 47e90ca2..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/EventComponent.kt +++ /dev/null @@ -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 { - val statusList = mutableListOf() - 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 - ) - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/VeranstaltungsListe.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/VeranstaltungsListe.kt deleted file mode 100644 index f75544d9..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/events/VeranstaltungsListe.kt +++ /dev/null @@ -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 = 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() - 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 - ) - } - } - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/horses/PferdeListe.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/components/horses/PferdeListe.kt deleted file mode 100644 index 4d7dc689..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/horses/PferdeListe.kt +++ /dev/null @@ -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 = 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() - 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() - 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() - 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 - ) - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/masterdata/StammdatenListe.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/components/masterdata/StammdatenListe.kt deleted file mode 100644 index ecaa8b8c..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/components/masterdata/StammdatenListe.kt +++ /dev/null @@ -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 = 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() - 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 - ) - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/config/ApiConfig.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/config/ApiConfig.kt deleted file mode 100644 index efef9e5b..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/config/ApiConfig.kt +++ /dev/null @@ -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 -) diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/BaseClientRepository.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/BaseClientRepository.kt deleted file mode 100644 index 678350b5..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/BaseClientRepository.kt +++ /dev/null @@ -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( - protected val baseEndpoint: String -) { - - /** - * Finds an entity by its ID. - */ - protected suspend fun findEntityById(id: String, entityClass: Class): 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("$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 findAllActiveEntities(limit: Int, offset: Int, entityClass: Class): List { - return try { - @Suppress("UNCHECKED_CAST") - (ApiClient.get("$baseEndpoint?limit=$limit&offset=$offset") as? List) ?: emptyList() - } catch (e: Exception) { - logError("Failed to fetch active entities", e) - emptyList() - } - } - - /** - * Searches entities by name/search term. - */ - protected suspend fun searchEntities(searchTerm: String, limit: Int, entityClass: Class): List { - return try { - @Suppress("UNCHECKED_CAST") - (ApiClient.get("$baseEndpoint?search=$searchTerm&limit=$limit") as? List) ?: emptyList() - } catch (e: Exception) { - logError("Failed to search entities by term: $searchTerm", e) - emptyList() - } - } - - /** - * Searches entities by a specific field. - */ - protected suspend fun searchEntitiesByField( - fieldName: String, - fieldValue: String, - limit: Int, - entityClass: Class - ): List { - return try { - @Suppress("UNCHECKED_CAST") - (ApiClient.get("$baseEndpoint?$fieldName=$fieldValue&limit=$limit") as? List) ?: emptyList() - } catch (e: Exception) { - logError("Failed to search entities by $fieldName: $fieldValue", e) - emptyList() - } - } - - /** - * Searches entities by date range. - */ - protected suspend fun searchEntitiesByDateRange( - startDate: String, - endDate: String, - limit: Int, - entityClass: Class - ): List { - return try { - @Suppress("UNCHECKED_CAST") - (ApiClient.get("$baseEndpoint?startDate=$startDate&endDate=$endDate&limit=$limit") as? List) ?: 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 saveEntity(entity: T, getId: (T) -> String, entityClass: Class): T { - return try { - val id = getId(entity) - if (id.isBlank()) { - // Create new entity - @Suppress("UNCHECKED_CAST") - ApiClient.post(baseEndpoint, entity as Any) as T - } else { - // Update existing entity - @Suppress("UNCHECKED_CAST") - ApiClient.put("$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("$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("$baseEndpoint/count") ?: 0L - } catch (e: Exception) { - logError("Failed to count active entities", e) - 0L - } - } - - /** - * Gets entities from a specific sub-endpoint. - */ - protected suspend fun getFromSubEndpoint(subEndpoint: String, limit: Int, entityClass: Class): List { - return try { - @Suppress("UNCHECKED_CAST") - (ApiClient.get("$baseEndpoint/$subEndpoint?limit=$limit") as? List) ?: 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() - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientEventRepository.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientEventRepository.kt deleted file mode 100644 index 03b21e4b..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientEventRepository.kt +++ /dev/null @@ -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("$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 { - return try { - ApiClient.get>("$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 { - return try { - ApiClient.get>("$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 { - return try { - ApiClient.get>("$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 { - return try { - ApiClient.get>("$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 { - return try { - ApiClient.get>("$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(baseEndpoint, event) - } else { - // Update existing event - ApiClient.put("$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("$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("$baseEndpoint/count") ?: 0L - } catch (e: Exception) { - println("[ERROR] Failed to count active events: ${e.message}") - 0L - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientPersonRepository.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientPersonRepository.kt deleted file mode 100644 index b1bd9d4b..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/ClientPersonRepository.kt +++ /dev/null @@ -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("$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 { - return try { - ApiClient.get>("$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 { - return try { - ApiClient.get>("$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(baseEndpoint, person) - } else { - // Update existing person - ApiClient.put("$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("$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("$baseEndpoint/count") ?: 0L - } catch (e: Exception) { - println("[ERROR] Failed to count active persons: ${e.message}") - 0L - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Event.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Event.kt deleted file mode 100644 index 9cc70322..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Event.kt +++ /dev/null @@ -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 = 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 - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/EventRepository.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/EventRepository.kt deleted file mode 100644 index 0a530df7..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/EventRepository.kt +++ /dev/null @@ -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 - - /** - * 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 - - /** - * 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 - - /** - * 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 - - /** - * Finds upcoming events. - * - * @param limit Maximum number of results to return - * @return List of upcoming events - */ - suspend fun findUpcoming(limit: Int = 50): List - - /** - * 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 -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/OptimizedClientPersonRepository.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/OptimizedClientPersonRepository.kt deleted file mode 100644 index 63ee37cd..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/OptimizedClientPersonRepository.kt +++ /dev/null @@ -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("/api/persons"), PersonRepository { - - override suspend fun findById(id: String): Person? { - return try { - ApiClient.get("$baseEndpoint/$id") - } catch (e: Exception) { - logError("Failed to fetch person with ID $id", e) - null - } - } - - override suspend fun findAllActive(limit: Int, offset: Int): List { - return try { - ApiClient.get>("$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 { - return try { - ApiClient.get>("$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(baseEndpoint, person) - } else { - // Update existing person - ApiClient.put("$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("$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() - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Person.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Person.kt deleted file mode 100644 index 9157b338..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/Person.kt +++ /dev/null @@ -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") - } - } -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/PersonRepository.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/PersonRepository.kt deleted file mode 100644 index 717a1f23..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/repository/PersonRepository.kt +++ /dev/null @@ -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 - - /** - * 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 - - /** - * 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 -} diff --git a/client/common-ui/src/main/kotlin/at/mocode/client/common/theme/Theme.kt b/client/common-ui/src/main/kotlin/at/mocode/client/common/theme/Theme.kt deleted file mode 100644 index 44fa61a6..00000000 --- a/client/common-ui/src/main/kotlin/at/mocode/client/common/theme/Theme.kt +++ /dev/null @@ -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 - ) -} diff --git a/client/desktop-app/build.gradle.kts b/client/desktop-app/build.gradle.kts index 23c8ffd8..245b842a 100644 --- a/client/desktop-app/build.gradle.kts +++ b/client/desktop-app/build.gradle.kts @@ -1,25 +1,31 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + plugins { - kotlin("jvm") alias(libs.plugins.compose.multiplatform) alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.multiplatform) } -dependencies { - // Greift explizit auf den "desktop" (JVM) Teil unseres KMP-Moduls zu. - implementation(projects.client.commonUi) +kotlin { + jvm("desktop") - // Stellt die Desktop-spezifischen Teile von Jetpack Compose bereit. - implementation(compose.desktop.currentOs) - - // Stellt die Coroutine-Integration für die Swing-UI-Bibliothek bereit. - implementation(libs.kotlinx.coroutines.swing) - - // --- Testing --- - testImplementation(projects.platform.platformTesting) + sourceSets { + val desktopMain by getting { + dependencies { + implementation(libs.compose.desktop.currentOs) + implementation(project(":client:common-ui")) + } + } + } } compose.desktop { application { - mainClass = "at.mocode.client.desktop.MainKt" + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "MeldestellePro" + packageVersion = "1.0.0" + } } } diff --git a/client/desktop-app/src/main/kotlin/Main.kt b/client/desktop-app/src/main/kotlin/Main.kt new file mode 100644 index 00000000..c9519583 --- /dev/null +++ b/client/desktop-app/src/main/kotlin/Main.kt @@ -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() + } +} diff --git a/client/desktop-app/src/main/kotlin/at/mocode/client/desktop/App.kt b/client/desktop-app/src/main/kotlin/at/mocode/client/desktop/App.kt deleted file mode 100644 index 8b59c4b0..00000000 --- a/client/desktop-app/src/main/kotlin/at/mocode/client/desktop/App.kt +++ /dev/null @@ -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(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 = 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") - } - } - } -} diff --git a/client/desktop-app/src/main/kotlin/at/mocode/client/desktop/main.kt b/client/desktop-app/src/main/kotlin/at/mocode/client/desktop/main.kt deleted file mode 100644 index 878a429e..00000000 --- a/client/desktop-app/src/main/kotlin/at/mocode/client/desktop/main.kt +++ /dev/null @@ -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() - } -} diff --git a/client/web-app/build.gradle.kts b/client/web-app/build.gradle.kts index 9425dce5..3c655b1e 100644 --- a/client/web-app/build.gradle.kts +++ b/client/web-app/build.gradle.kts @@ -7,9 +7,29 @@ plugins { kotlin { js(IR) { browser { - // Konfiguriert den Development-Server und die finalen Bundles. commonWebpackConfig { - outputFileName = "MeldestelleWebApp.js" + devServer = org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig.DevServer( + open = true, + port = 8081 + ) + } + webpackTask { + cssSupport { + enabled.set(true) + } + } + runTask { + cssSupport { + enabled.set(true) + } + } + testTask { + useKarma { + useChromeHeadless() + webpackConfig.cssSupport { + enabled.set(true) + } + } } } binaries.executable() @@ -18,23 +38,9 @@ kotlin { sourceSets { val jsMain by getting { dependencies { - // Greift explizit auf den JS-Teil unseres KMP-Moduls zu. - implementation(projects.client.commonUi) - - // Stellt die Web-spezifischen (HTML) Teile von Jetpack Compose bereit. - implementation(compose.html.core) - - // HTTP client for making requests to the backend - implementation(libs.kotlinx.coroutines.core) - implementation(libs.ktor.client.js) - implementation(libs.ktor.client.contentNegotiation) - implementation(libs.ktor.client.serialization.kotlinx.json) - } - } - val jsTest by getting { - dependencies { - implementation(libs.kotlin.test) + implementation(project(":client:common-ui")) } } } } + diff --git a/client/web-app/src/jsMain/kotlin/Main.kt b/client/web-app/src/jsMain/kotlin/Main.kt new file mode 100644 index 00000000..9bbdbca7 --- /dev/null +++ b/client/web-app/src/jsMain/kotlin/Main.kt @@ -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() + } +} + diff --git a/client/web-app/src/jsMain/resources/index.html b/client/web-app/src/jsMain/resources/index.html new file mode 100644 index 00000000..57ad3c44 --- /dev/null +++ b/client/web-app/src/jsMain/resources/index.html @@ -0,0 +1,12 @@ + + + + + Meldestelle Pro + + + + + + + diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/App.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/App.kt deleted file mode 100644 index 45d895bd..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/App.kt +++ /dev/null @@ -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(null) } - var isLoading by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf(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(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") } - } - } - } - } -} diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/di/AppDependencies.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/di/AppDependencies.kt deleted file mode 100644 index 69cbdb34..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/di/AppDependencies.kt +++ /dev/null @@ -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") - } -} diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/main.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/main.kt deleted file mode 100644 index 9be655f1..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/main.kt +++ /dev/null @@ -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() - } -} diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/screens/CreatePersonScreen.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/screens/CreatePersonScreen.kt deleted file mode 100644 index 015eec59..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/screens/CreatePersonScreen.kt +++ /dev/null @@ -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") - } - } - } - } - } -} diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/screens/PersonListScreen.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/screens/PersonListScreen.kt deleted file mode 100644 index bfc1a89a..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/screens/PersonListScreen.kt +++ /dev/null @@ -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 - ) - } - } - } -} diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModel.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModel.kt deleted file mode 100644 index eaf77e40..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModel.kt +++ /dev/null @@ -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(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 - } -} diff --git a/client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/PersonListViewModel.kt b/client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/PersonListViewModel.kt deleted file mode 100644 index 3bc69fa6..00000000 --- a/client/web-app/src/main/kotlin/at/mocode/client/web/viewmodel/PersonListViewModel.kt +++ /dev/null @@ -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>(emptyList()) - private set - var isLoading by mutableStateOf(false) - private set - var errorMessage by mutableStateOf(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 -) diff --git a/client/web-app/src/main/resources/MeldestelleWebApp.js b/client/web-app/src/main/resources/MeldestelleWebApp.js deleted file mode 100644 index e69de29b..00000000 diff --git a/client/web-app/src/main/resources/index.html b/client/web-app/src/main/resources/index.html deleted file mode 100644 index fc0c71e7..00000000 --- a/client/web-app/src/main/resources/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Meldestelle - Reitersport Management - - - -
- - - diff --git a/client/web-app/src/test/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModelTest.kt b/client/web-app/src/test/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModelTest.kt deleted file mode 100644 index 1747f019..00000000 --- a/client/web-app/src/test/kotlin/at/mocode/client/web/viewmodel/CreatePersonViewModelTest.kt +++ /dev/null @@ -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() - - 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 { - 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 { - 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()) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9de619cf..9e28238d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,7 @@ reactorKafka = "1.3.22" jackson = "2.17.0" jakartaAnnotation = "2.1.1" roomCommonJvm = "2.7.2" +uiDesktop = "1.7.0" [libraries] # --- 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" } 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" } +ui-desktop = { group = "androidx.compose.ui", name = "ui-desktop", version.ref = "uiDesktop" } [bundles] # OPTIMIERUNG: Bündelt gängige Abhängigkeitsgruppen. diff --git a/settings.gradle.kts b/settings.gradle.kts index 93e8baa2..cd1b69ac 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,9 @@ pluginManagement { } } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} dependencyResolutionManagement { @@ -85,11 +88,5 @@ include(":masterdata:masterdata-infrastructure") include(":masterdata:masterdata-api") 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 - */