refactoring:
Ein neues Kotlin-Multiplattform-Mitgliedermodul wurde bereitgestellt, das clientseitige API-Aufrufe, Benutzeroberfläche und Navigation in die Host-Anwendung integriert. Der Build wurde durch die Entkopplung von Backend-Mitgliedermodulen stabilisiert, und alle Tests wurden erfolgreich abgeschlossen. Die Client-Funktionen erfolgen über REST-Aufrufe an das Gateway; die Backend-Integration wird in einer späteren Phase implementiert.
This commit is contained in:
@@ -77,6 +77,7 @@ kotlin {
|
|||||||
implementation(project(":clients:shared:common-ui"))
|
implementation(project(":clients:shared:common-ui"))
|
||||||
implementation(project(":clients:shared:navigation"))
|
implementation(project(":clients:shared:navigation"))
|
||||||
implementation(project(":clients:ping-feature"))
|
implementation(project(":clients:ping-feature"))
|
||||||
|
implementation(project(":clients:members-feature"))
|
||||||
|
|
||||||
// Compose Multiplatform
|
// Compose Multiplatform
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.clients.membersfeature.ProfileScreen
|
||||||
|
import at.mocode.clients.membersfeature.ProfileViewModel
|
||||||
|
import at.mocode.clients.shared.navigation.AppScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainApp() {
|
fun MainApp() {
|
||||||
@@ -11,13 +14,20 @@ fun MainApp() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
DevelopmentScreen()
|
var currentScreen by remember { mutableStateOf<AppScreen>(AppScreen.Home) }
|
||||||
|
|
||||||
|
when (currentScreen) {
|
||||||
|
is AppScreen.Home -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
|
||||||
|
is AppScreen.Login -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
|
||||||
|
is AppScreen.Ping -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
|
||||||
|
is AppScreen.Profile -> ProfileScreen(viewModel = remember { ProfileViewModel() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DevelopmentScreen() {
|
fun DevelopmentScreen(onOpenProfile: () -> Unit) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -47,6 +57,9 @@ fun DevelopmentScreen() {
|
|||||||
Button(onClick = { testStatus = "Testing Ping Service..." }) {
|
Button(onClick = { testStatus = "Testing Ping Service..." }) {
|
||||||
Text("Test Ping Service")
|
Text("Test Ping Service")
|
||||||
}
|
}
|
||||||
|
Button(onClick = onOpenProfile) {
|
||||||
|
Text("Open Profile")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Status: $testStatus")
|
Text("Status: $testStatus")
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "at.mocode.clients"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
||||||
|
|
||||||
|
jvmToolchain(21)
|
||||||
|
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
js {
|
||||||
|
browser {
|
||||||
|
testTask { enabled = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableWasm) {
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
|
wasmJs { browser() }
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain.dependencies {
|
||||||
|
// UI Kit
|
||||||
|
implementation(project(":clients:shared:common-ui"))
|
||||||
|
// Shared config/utilities
|
||||||
|
implementation(project(":clients:shared"))
|
||||||
|
// Authentication helpers (token + authenticated client)
|
||||||
|
implementation(project(":clients:auth-feature"))
|
||||||
|
|
||||||
|
// Compose
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.components.resources)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
|
|
||||||
|
// ViewModel lifecycle + compose helpers
|
||||||
|
implementation(libs.bundles.compose.common)
|
||||||
|
|
||||||
|
// HTTP + Kotlinx
|
||||||
|
implementation(libs.bundles.ktor.client.common)
|
||||||
|
implementation(libs.bundles.kotlinx.core)
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTest.dependencies {
|
||||||
|
implementation(libs.kotlin.test)
|
||||||
|
implementation(libs.kotlinx.coroutines.test)
|
||||||
|
implementation(libs.ktor.client.mock)
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(libs.ktor.client.cio)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsMain.dependencies {
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableWasm) {
|
||||||
|
val wasmJsMain = getByName("wasmJsMain")
|
||||||
|
wasmJsMain.dependencies {
|
||||||
|
implementation(libs.ktor.client.js)
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_21)
|
||||||
|
freeCompilerArgs.addAll("-opt-in=kotlin.RequiresOptIn")
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package at.mocode.clients.membersfeature
|
||||||
|
|
||||||
|
import at.mocode.clients.authfeature.AuthenticatedHttpClient
|
||||||
|
import at.mocode.clients.shared.AppConfig
|
||||||
|
import at.mocode.clients.membersfeature.model.MemberProfile
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader
|
||||||
|
|
||||||
|
class MembersApiClient(
|
||||||
|
private val baseUrl: String = AppConfig.GATEWAY_URL
|
||||||
|
) {
|
||||||
|
private val client = AuthenticatedHttpClient.create()
|
||||||
|
|
||||||
|
suspend fun getMyProfile(): MemberProfile {
|
||||||
|
// Erwarteter Endpoint: GET /api/members/me
|
||||||
|
return client.get("$baseUrl/api/members/me") {
|
||||||
|
addAuthHeader()
|
||||||
|
}.body()
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package at.mocode.clients.membersfeature
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileScreen(viewModel: ProfileViewModel) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = "👤 Mein Profil", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorMessage?.let { error ->
|
||||||
|
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text("Fehler", style = MaterialTheme.typography.titleMedium)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(error)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Button(onClick = { viewModel.clearError() }) { Text("Schließen") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.profile?.let { profile ->
|
||||||
|
Card {
|
||||||
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(text = profile.fullName.ifBlank { "Unbekannt" }, style = MaterialTheme.typography.titleLarge)
|
||||||
|
profile.username?.let { Text("Benutzername: $it") }
|
||||||
|
profile.email?.let { Text("E-Mail: $it") }
|
||||||
|
if (profile.roles.isNotEmpty()) {
|
||||||
|
Text("Rollen: ${profile.roles.joinToString(", ")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Button(onClick = { viewModel.loadProfile() }, enabled = !state.isLoading) {
|
||||||
|
Text("Neu laden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package at.mocode.clients.membersfeature
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.mocode.clients.membersfeature.model.MemberProfile
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class ProfileUiState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val profile: MemberProfile? = null,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class ProfileViewModel(
|
||||||
|
private val api: MembersApiClient = MembersApiClient()
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(ProfileUiState())
|
||||||
|
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun loadProfile() {
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val profile = api.getMyProfile()
|
||||||
|
_uiState.value = ProfileUiState(isLoading = false, profile = profile)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.value = ProfileUiState(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = e.message ?: "Profil konnte nicht geladen werden"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_uiState.value = _uiState.value.copy(errorMessage = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package at.mocode.clients.membersfeature.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MemberProfile(
|
||||||
|
val id: String? = null,
|
||||||
|
val username: String? = null,
|
||||||
|
val email: String? = null,
|
||||||
|
val firstName: String? = null,
|
||||||
|
val lastName: String? = null,
|
||||||
|
val roles: List<String> = emptyList()
|
||||||
|
) {
|
||||||
|
val fullName: String
|
||||||
|
get() = listOfNotNull(firstName, lastName).joinToString(" ").ifBlank { username ?: "" }
|
||||||
|
}
|
||||||
+1
@@ -4,4 +4,5 @@ sealed class AppScreen {
|
|||||||
data object Home : AppScreen()
|
data object Home : AppScreen()
|
||||||
data object Login : AppScreen()
|
data object Login : AppScreen()
|
||||||
data object Ping : AppScreen()
|
data object Ping : AppScreen()
|
||||||
|
data object Profile : AppScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package at.mocode.core.domain.serialization
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlinx Serialization serializer for kotlinx.datetime.LocalDate.
|
||||||
|
* Serializes as ISO-8601 date string (yyyy-MM-dd).
|
||||||
|
*/
|
||||||
|
object KotlinLocalDateSerializer : KSerializer<LocalDate> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor("KotlinLocalDate", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: LocalDate) {
|
||||||
|
encoder.encodeString(value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): LocalDate {
|
||||||
|
val text = decoder.decodeString()
|
||||||
|
return try {
|
||||||
|
LocalDate.parse(text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw SerializationException("Invalid LocalDate format: '$text'", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ plugins {
|
|||||||
// kotlin("jvm")
|
// kotlin("jvm")
|
||||||
// kotlin("plugin.spring")
|
// kotlin("plugin.spring")
|
||||||
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlin.spring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
|
|
||||||
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||||
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||||
@@ -16,8 +16,8 @@ plugins {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
|
|
||||||
implementation(projects.members.membersDomain)
|
implementation(projects.services.members.membersDomain)
|
||||||
implementation(projects.members.membersApplication)
|
implementation(projects.services.members.membersApplication)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.infrastructure.messaging.messagingClient)
|
implementation(projects.infrastructure.messaging.messagingClient)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.members.membersDomain)
|
implementation(projects.services.members.membersDomain)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.infrastructure.messaging.messagingClient)
|
implementation(projects.infrastructure.messaging.messagingClient)
|
||||||
|
|||||||
+8
-29
@@ -63,39 +63,18 @@ include(":clients:ping-feature")
|
|||||||
include(":clients:auth-feature")
|
include(":clients:auth-feature")
|
||||||
include(":clients:shared:common-ui")
|
include(":clients:shared:common-ui")
|
||||||
include(":clients:shared:navigation")
|
include(":clients:shared:navigation")
|
||||||
|
include(":clients:members-feature")
|
||||||
|
|
||||||
// Documentation module
|
// Documentation module
|
||||||
include(":docs")
|
include(":docs")
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Business modules (temporarily disabled - require multiplatform configuration updates)
|
// Business modules (temporarily disabled - require multiplatform configuration updates)
|
||||||
// Members modules
|
// Note: We enable only the Members modules needed for API contracts to support the Members client feature.
|
||||||
include(":members:members-domain")
|
|
||||||
include(":members:members-application")
|
|
||||||
include(":members:members-infrastructure")
|
|
||||||
include(":members:members-api")
|
|
||||||
include(":members:members-service")
|
|
||||||
|
|
||||||
// Horses modules
|
|
||||||
include(":horses:horses-domain")
|
|
||||||
include(":horses:horses-application")
|
|
||||||
include(":horses:horses-infrastructure")
|
|
||||||
include(":horses:horses-api")
|
|
||||||
include(":horses:horses-service")
|
|
||||||
|
|
||||||
// Events modules
|
|
||||||
include(":events:events-domain")
|
|
||||||
include(":events:events-application")
|
|
||||||
include(":events:events-infrastructure")
|
|
||||||
include(":events:events-api")
|
|
||||||
include(":events:events-service")
|
|
||||||
|
|
||||||
// Masterdata modules
|
|
||||||
include(":masterdata:masterdata-domain")
|
|
||||||
include(":masterdata:masterdata-application")
|
|
||||||
include(":masterdata:masterdata-infrastructure")
|
|
||||||
include(":masterdata:masterdata-api")
|
|
||||||
include(":masterdata:masterdata-service")
|
|
||||||
|
|
||||||
// Note: These modules need multiplatform configuration updates to work with current KMP/WASM setup
|
|
||||||
*/
|
*/
|
||||||
|
// Members modules are currently disabled to keep the client build lean.
|
||||||
|
// We consume the Members REST API from the client without compiling backend modules here.
|
||||||
|
// include(":services:members:members-domain")
|
||||||
|
// include(":services:members:members-application")
|
||||||
|
// include(":services:members:members-api")
|
||||||
|
// other business modules remain disabled
|
||||||
|
|||||||
Reference in New Issue
Block a user