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:
parent
d11ee48fde
commit
a6a35a2eda
|
|
@ -77,6 +77,7 @@ kotlin {
|
|||
implementation(project(":clients:shared:common-ui"))
|
||||
implementation(project(":clients:shared:navigation"))
|
||||
implementation(project(":clients:ping-feature"))
|
||||
implementation(project(":clients:members-feature"))
|
||||
|
||||
// Compose Multiplatform
|
||||
implementation(compose.runtime)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
fun MainApp() {
|
||||
|
|
@ -11,13 +14,20 @@ fun MainApp() {
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
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
|
||||
fun DevelopmentScreen() {
|
||||
fun DevelopmentScreen(onOpenProfile: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -47,6 +57,9 @@ fun DevelopmentScreen() {
|
|||
Button(onClick = { testStatus = "Testing Ping Service..." }) {
|
||||
Text("Test Ping Service")
|
||||
}
|
||||
Button(onClick = onOpenProfile) {
|
||||
Text("Open Profile")
|
||||
}
|
||||
}
|
||||
|
||||
Text("Status: $testStatus")
|
||||
|
|
|
|||
87
clients/members-feature/build.gradle.kts
Normal file
87
clients/members-feature/build.gradle.kts
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?: "" }
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ sealed class AppScreen {
|
|||
data object Home : AppScreen()
|
||||
data object Login : AppScreen()
|
||||
data object Ping : AppScreen()
|
||||
data object Profile : AppScreen()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("plugin.spring")
|
||||
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
|
||||
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||
|
|
@ -16,8 +16,8 @@ plugins {
|
|||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.members.membersDomain)
|
||||
implementation(projects.members.membersApplication)
|
||||
implementation(projects.services.members.membersDomain)
|
||||
implementation(projects.services.members.membersApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.members.membersDomain)
|
||||
implementation(projects.services.members.membersDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
|
|
|
|||
|
|
@ -63,39 +63,18 @@ include(":clients:ping-feature")
|
|||
include(":clients:auth-feature")
|
||||
include(":clients:shared:common-ui")
|
||||
include(":clients:shared:navigation")
|
||||
include(":clients:members-feature")
|
||||
|
||||
// Documentation module
|
||||
include(":docs")
|
||||
|
||||
/*
|
||||
// Business modules (temporarily disabled - require multiplatform configuration updates)
|
||||
// Members modules
|
||||
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
|
||||
// Note: We enable only the Members modules needed for API contracts to support the Members client feature.
|
||||
*/
|
||||
// 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user