feat(online-nennung): integrate online nomination workflow via REST and mail service

- Enabled web-to-backend nominations with `MailController` and REST endpoint (`/api/mail/nennung`).
- Added `NennungRemoteRepository` for frontend API integration using Ktor.
- Linked `WebMainScreen` to backend API for nomination handling and confirmation display.
- Implemented automated confirmation emails for received nominations.
- Updated `MASTER_ROADMAP` to reflect progress on Phase 13 milestones.
- Improved Nennung UI, backend persistence, and QA tracking for Neumarkt tournament.
This commit is contained in:
2026-04-15 10:37:07 +02:00
parent b4c400efea
commit c542094196
8 changed files with 193 additions and 4 deletions
@@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
}
group = "at.mocode.clients"
@@ -54,6 +55,8 @@ kotlin {
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.bundles.ktor.client.common)
}
jvmMain.dependencies {
@@ -1,9 +1,12 @@
package at.mocode.frontend.features.nennung.di
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
import io.ktor.client.HttpClient
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val nennungFeatureModule = module {
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>()) }
viewModel { NennungViewModel() }
}
@@ -0,0 +1,52 @@
package at.mocode.frontend.features.nennung.domain
import at.mocode.frontend.features.nennung.presentation.web.NennungPayload
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
import kotlinx.serialization.Serializable
@Serializable
data class NennungApiRequest(
val turnierNr: String,
val vorname: String,
val nachname: String,
val lizenz: String,
val pferdName: String,
val pferdAlter: String,
val email: String,
val telefon: String?,
val bewerbe: String,
val bemerkungen: String?
)
class NennungRemoteRepository(private val client: HttpClient) {
suspend fun sendeNennung(turnierNr: String, payload: NennungPayload): Result<Unit> {
return try {
val request = NennungApiRequest(
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 senden direkt an den mail-service (Port 8085)
// In einer Prod-Umgebung würde dies über das Gateway laufen.
client.post("http://localhost:8085/api/mail/nennung") {
contentType(ContentType.Application.Json)
setBody(request)
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
@@ -761,7 +761,8 @@ private fun VerkaufTabInhalt(artikel: List<VerkaufArtikel>, onMengeChanged: (Ver
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Remove, contentDescription = "", modifier = Modifier.size(12.dp))
}
Text(art.buchungstext,
Text(
art.buchungstext,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
@@ -15,13 +15,18 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.features.billing.presentation.BillingViewModel
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
import at.mocode.frontend.features.nennung.presentation.web.OnlineNennungFormular
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WebMainScreen() {
val billingViewModel: BillingViewModel = koinViewModel()
val nennungRepository: NennungRemoteRepository = koinInject()
val scope = rememberCoroutineScope()
var currentScreen by remember { mutableStateOf<WebScreen>(WebScreen.Landing) }
Scaffold(
@@ -48,9 +53,15 @@ fun WebMainScreen() {
is WebScreen.Nennung -> OnlineNennungFormular(
turnierNr = screen.turnierId.toString(),
onNennenAbgeschickt = { payload ->
// Hier wird später der Mail-Versand oder API-Call integriert
println("Nennung abgeschickt: $payload")
currentScreen = WebScreen.Erfolg(payload.email)
scope.launch {
val result = nennungRepository.sendeNennung(screen.turnierId.toString(), payload)
if (result.isSuccess) {
currentScreen = WebScreen.Erfolg(payload.email)
} else {
// Hier könnte man eine Fehlermeldung anzeigen
println("Fehler beim Senden der Nennung: ${result.exceptionOrNull()?.message}")
}
}
},
onBack = { currentScreen = WebScreen.Landing }
)