diff --git a/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.jvm.kt b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.jvm.kt index f11eaf48..2c761750 100644 --- a/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.jvm.kt +++ b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.jvm.kt @@ -13,7 +13,7 @@ actual object PlatformConfig { actual fun resolveMailServiceUrl(): String { val env = System.getenv("MAIL_SERVICE_URL")?.trim().orEmpty() if (env.isNotEmpty()) return env.removeSuffix("/") - return "http://localhost:8083" + return "http://localhost:8092" } actual fun resolveKeycloakUrl(): String { diff --git a/frontend/core/network/src/wasmJsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.wasmJs.kt b/frontend/core/network/src/wasmJsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.wasmJs.kt index 9a27b0f4..b9257c05 100644 --- a/frontend/core/network/src/wasmJsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.wasmJs.kt +++ b/frontend/core/network/src/wasmJsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.wasmJs.kt @@ -9,7 +9,7 @@ actual object PlatformConfig { actual fun resolveMailServiceUrl(): String { val fromGlobal = getGlobalMailServiceUrl() if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/") - return "http://localhost:8085" + return "http://localhost:8092" } actual fun resolveKeycloakUrl(): String { diff --git a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt index a8ce77ad..5ee1607d 100644 --- a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt +++ b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/di/NennungModule.kt @@ -11,5 +11,5 @@ import org.koin.dsl.module val nennungFeatureModule = module { single { NennungRemoteRepository(get(named("apiClient"))) } viewModel { NennungViewModel() } - viewModel { OnlineNennungViewModel(get(named("apiClient"))) } + viewModel { OnlineNennungViewModel(get()) } } diff --git a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungFormular.kt b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungFormular.kt index 5db37f06..b684f73b 100644 --- a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungFormular.kt +++ b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungFormular.kt @@ -1,12 +1,8 @@ package at.mocode.frontend.features.nennung.presentation.web import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -17,7 +13,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import at.mocode.frontend.core.designsystem.theme.AppColors import at.mocode.frontend.features.nennung.domain.Bewerb -import at.mocode.frontend.features.nennung.domain.NennungMockData +import at.mocode.frontend.features.nennung.domain.Sparte data class NennungPayload( val vorname: String, @@ -39,271 +35,92 @@ fun OnlineNennungFormular( ) { var vorname by remember { mutableStateOf("") } var nachname by remember { mutableStateOf("") } - var lizenz by remember { mutableStateOf("Lizenzfrei") } - var pferdName by remember { mutableStateOf("") } - var pferdAlter by remember { mutableStateOf("2020") } var email by remember { mutableStateOf("") } - var telefon by remember { mutableStateOf("") } - var bemerkungen by remember { mutableStateOf("") } - var dsgvoAkzeptiert by remember { mutableStateOf(false) } - - val ausgewaehlteBewerbe = remember { mutableStateListOf() } - - val lizenzen = listOf("Lizenzfrei", "R1", "R2", "R3", "R4", "RS1", "RS2") - val jahre = (2000..2022).map { it.toString() }.reversed() val isEmailValid = email.contains("@") && email.contains(".") - val canSubmit = vorname.isNotBlank() && - nachname.isNotBlank() && - pferdName.isNotBlank() && - isEmailValid && - ausgewaehlteBewerbe.isNotEmpty() && - dsgvoAkzeptiert + val canSubmit = vorname.isNotBlank() && nachname.isNotBlank() && isEmailValid - // Clean-White Layout: Hintergrund hellgrau, Formular in weißen Cards - Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF8F9FA))) { - LazyColumn( - modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier.fillMaxSize().background(Color(0xFFF8F9FA)), + contentAlignment = Alignment.Center + ) { + Card( + modifier = Modifier.width(400.dp).padding(16.dp), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { - item { - Spacer(Modifier.height(32.dp)) + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { Text( - text = "Turnier Online-Nennung", + text = "Hallo Du! 👋", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.ExtraBold, - color = Color(0xFF2D3436) + color = AppColors.Primary ) Text( - text = "Turnier-Nr: $turnierNr", - style = MaterialTheme.typography.bodyLarge, - color = Color.Gray, - modifier = Modifier.padding(bottom = 24.dp) - ) - } - - // --- REITER CARD --- - item { - FormCard("Persönliche Daten (Reiter)") { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - ModernTextField(vorname, { vorname = it }, "Vorname *", Modifier.weight(1f)) - ModernTextField(nachname, { nachname = it }, "Nachname *", Modifier.weight(1f)) - } - - Text("Lizenzklasse", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold) - DropdownSelector(lizenz, lizenzen) { lizenz = it } - } - } - } - - // --- PFERD CARD --- - item { - FormCard("Pferdedaten") { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - ModernTextField(pferdName, { pferdName = it }, "Name oder Kopfnummer *") - - Text("Geburtsjahr", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold) - DropdownSelector(pferdAlter, jahre) { pferdAlter = it } - } - } - } - - // --- KONTAKT CARD --- - item { - FormCard("Kontakt für Rückfragen") { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - ModernTextField( - value = email, - onValueChange = { email = it }, - label = "E-Mail Adresse *", - isError = email.isNotBlank() && !isEmailValid - ) - ModernTextField(telefon, { telefon = it }, "Telefonnummer (optional)") - } - } - } - - // --- BEWERBE CARD --- - item { - FormCard("Bewerbe & Prüfungen") { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - NennungMockData.bewerbe.forEach { bewerb -> - val isSelected = ausgewaehlteBewerbe.any { it.nr == bewerb.nr } - BewerbRow(bewerb, isSelected) { - if (isSelected) { - val item = ausgewaehlteBewerbe.find { it.nr == bewerb.nr } - if (item != null) ausgewaehlteBewerbe.remove(item) - } else { - ausgewaehlteBewerbe.add(bewerb) - } - } - } - } - } - } - - // --- WÜNSCHE CARD --- - item { - FormCard("Anmerkungen") { - OutlinedTextField( - value = bemerkungen, - onValueChange = { bemerkungen = it }, - placeholder = { Text("Besondere Wünsche, Stallplaketten, etc.") }, - modifier = Modifier.fillMaxWidth().height(120.dp), - shape = RoundedCornerShape(12.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = AppColors.Primary, - unfocusedBorderColor = Color(0xFFE0E0E0) - ) - ) - } - } - - // --- DSGVO & ABSCHLUSS --- - item { - Column( - modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { dsgvoAkzeptiert = !dsgvoAkzeptiert }.padding(8.dp) - ) { - Checkbox(checked = dsgvoAkzeptiert, onCheckedChange = { dsgvoAkzeptiert = it }) - Spacer(Modifier.width(8.dp)) - Text( - "Ich akzeptiere die Datenschutzbestimmungen.", - style = MaterialTheme.typography.bodyMedium - ) - } - - Spacer(Modifier.height(16.dp)) - - Button( - onClick = { - onNennenAbgeschickt( - NennungPayload( - vorname, nachname, lizenz, pferdName, pferdAlter, - email, telefon, ausgewaehlteBewerbe.toList(), bemerkungen - ) - ) - }, - enabled = canSubmit, - modifier = Modifier.fillMaxWidth().height(56.dp), - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (canSubmit) Color(0xFF2ECC71) else Color(0xFFBDC3C7) - ) - ) { - Text("JETZT NENNEN", fontWeight = FontWeight.Bold, fontSize = 16.sp) - } - - TextButton(onClick = onBack, modifier = Modifier.padding(top = 8.dp)) { - Text("Abbrechen", color = Color.Gray) - } - - Spacer(Modifier.height(48.dp)) - } - } - } - } -} - -@Composable -fun FormCard(title: String, content: @Composable () -> Unit) { - Card( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column(modifier = Modifier.padding(20.dp)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = AppColors.Primary, - modifier = Modifier.padding(bottom = 16.dp) - ) - content() - } - } -} - -@Composable -fun ModernTextField( - value: String, - onValueChange: (String) -> Unit, - label: String, - modifier: Modifier = Modifier, - isError: Boolean = false -) { - OutlinedTextField( - value = value, - onValueChange = onValueChange, - label = { Text(label) }, - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - isError = isError, - singleLine = true, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = AppColors.Primary, - unfocusedBorderColor = Color(0xFFE0E0E0), - errorBorderColor = Color.Red - ) - ) -} - -@Composable -fun DropdownSelector(current: String, options: List, onSelect: (String) -> Unit) { - var expanded by remember { mutableStateOf(false) } - Box { - OutlinedButton( - onClick = { expanded = true }, - modifier = Modifier.fillMaxWidth().height(56.dp), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Black), - border = ButtonDefaults.outlinedButtonBorder(enabled = true).copy(brush = androidx.compose.ui.graphics.SolidColor(Color(0xFFE0E0E0))) - ) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - Text(current) - Icon(Icons.Default.Info, null, modifier = Modifier.size(18.dp), tint = Color.LightGray) - } - } - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - options.forEach { opt -> - DropdownMenuItem(text = { Text(opt) }, onClick = { onSelect(opt); expanded = false }) - } - } - } -} - -@Composable -fun BewerbRow(bewerb: Bewerb, isSelected: Boolean, onClick: () -> Unit) { - Surface( - onClick = onClick, - shape = RoundedCornerShape(12.dp), - color = if (isSelected) Color(0xFFE8F5E9) else Color(0xFFF5F5F5), - modifier = Modifier.fillMaxWidth() - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(12.dp) - ) { - Checkbox(checked = isSelected, onCheckedChange = null) - Spacer(Modifier.width(12.dp)) - Column { - Text( - "Bewerb ${bewerb.nr}: ${bewerb.name}", - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - Text( - bewerb.tag, - style = MaterialTheme.typography.bodySmall, + text = "Lass uns Plan-B testen. Turnier: $turnierNr", + style = MaterialTheme.typography.bodyMedium, color = Color.Gray ) + + OutlinedTextField( + value = vorname, + onValueChange = { vorname = it }, + label = { Text("Vorname") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = nachname, + onValueChange = { nachname = it }, + label = { Text("Nachname") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("E-Mail Adresse") }, + singleLine = true, + isError = email.isNotEmpty() && !isEmailValid, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(Modifier.height(8.dp)) + + Button( + onClick = { + // Wir füllen den Rest mit Dummy-Daten für den Test + val payload = NennungPayload( + vorname = vorname, + nachname = nachname, + lizenz = "Lizenzfrei", + pferdName = "Test-Pferd (Plan-B)", + pferdAlter = "2020", + email = email, + telefon = "0123456789", + bewerbe = listOf(Bewerb(1, "Tag 1", 1, "08:00", "Test-Bewerb", Sparte.SPRINGEN, "A")), + bemerkungen = "Dies ist ein automatischer Test für Plan-B." + ) + onNennenAbgeschickt(payload) + }, + enabled = canSubmit, + modifier = Modifier.fillMaxWidth().height(50.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = AppColors.Primary) + ) { + Text("Jetzt schicken!", fontWeight = FontWeight.Bold, fontSize = 16.sp) + } + + TextButton(onClick = onBack) { + Text("Zurück", color = Color.Gray) + } } } } diff --git a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungViewModel.kt b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungViewModel.kt index 306e6107..0cab85b6 100644 --- a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungViewModel.kt +++ b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/web/OnlineNennungViewModel.kt @@ -2,31 +2,12 @@ package at.mocode.frontend.features.nennung.presentation.web import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.http.* +import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable - -@Serializable -data class NennungDto( - val id: String? = null, - val turnierNr: String, - val status: String = "NEU", - val vorname: String, - val nachname: String, - val lizenz: String, - val pferdName: String, - val pferdAlter: String, - val email: String, - val telefon: String?, - val bewerbe: String, // Als JSON-String oder Komma-separiert - val bemerkungen: String? -) data class OnlineNennungUiState( val isLoading: Boolean = false, @@ -35,7 +16,7 @@ data class OnlineNennungUiState( ) class OnlineNennungViewModel( - private val httpClient: HttpClient + private val nennungRepository: NennungRemoteRepository ) : ViewModel() { private val _uiState = MutableStateFlow(OnlineNennungUiState()) @@ -44,31 +25,11 @@ class OnlineNennungViewModel( fun sendeNennung(turnierNr: String, payload: NennungPayload) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, error = null) } - try { - val dto = NennungDto( - turnierNr = turnierNr, - vorname = payload.vorname, - nachname = payload.nachname, - lizenz = payload.lizenz, - pferdName = payload.pferdName, - pferdAlter = payload.pferdAlter, - email = payload.email, - telefon = payload.telefon, - bewerbe = payload.bewerbe.joinToString(",") { it.nr.toString() }, - bemerkungen = payload.bemerkungen - ) - - // Wir nutzen den httpClient, der via Koin injiziert wird. - // Da im Web-Frontend evtl. kein API-Gateway davor ist (oder ein anderes), - // konfigurieren wir den Pfad hier explizit. - httpClient.post("/api/mail/nennungen") { - contentType(ContentType.Application.Json) - setBody(dto) - } - + val result = nennungRepository.sendeNennung(turnierNr, payload) + if (result.isSuccess) { _uiState.update { it.copy(isLoading = false, isSuccess = true) } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, error = "Fehler beim Senden: ${e.message}") } + } else { + _uiState.update { it.copy(isLoading = false, error = "Fehler beim Senden: ${result.exceptionOrNull()?.message}") } } } }