feat(mail-service): initialize Mail-Service and integrate online nomination workflow
- Created `MailServiceApplication` with Spring Boot setup. - Added `MailPollingService` for IMAP polling, `TurnierNr` extraction, and auto-reply functionality. - Implemented structured email sending for online nominations via `OnlineNennungFormular`. - Updated frontend with `Erfolgsscreen` for nomination confirmation and fallback handling. - Added build configurations for Mail-Service and frontend nomination module. - Documented phase-based roadmap for Online-Nennung and Mail-Service rollout.
This commit is contained in:
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
/**
|
||||
* Feature-Modul: Nennungs-Maske (Desktop-only)
|
||||
* Kapselt die gesamte UI und Logik für die Nennungserfassung am Turnier.
|
||||
* kapselt die gesamte UI und Logik für die Nennungserfassung am Turnier.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package at.mocode.frontend.features.nennung.presentation.web
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.features.nennung.domain.Bewerb
|
||||
import at.mocode.frontend.features.nennung.domain.NennungMockData
|
||||
|
||||
@Composable
|
||||
fun OnlineNennungFormular(
|
||||
turnierNr: String,
|
||||
onNennenAbgeschickt: (NennungPayload) -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
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<Bewerb>() }
|
||||
|
||||
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
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
Text("Online-Nennung für Turnier $turnierNr", style = MaterialTheme.typography.headlineSmall)
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
}
|
||||
|
||||
// Reiter Daten
|
||||
item {
|
||||
Text("Reiter", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = vorname,
|
||||
onValueChange = { vorname = it },
|
||||
label = { Text("Vorname *") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = nachname,
|
||||
onValueChange = { nachname = it },
|
||||
label = { Text("Nachname *") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text("Lizenz", style = MaterialTheme.typography.titleSmall)
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(lizenz)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
lizenzen.forEach { l ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(l) },
|
||||
onClick = { lizenz = l; expanded = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pferd Daten
|
||||
item {
|
||||
Text("Pferd", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(
|
||||
value = pferdName,
|
||||
onValueChange = { pferdName = it },
|
||||
label = { Text("Pferd-Name oder Kopfnummer *") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text("Geburtsjahr Pferd", style = MaterialTheme.typography.titleSmall)
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(pferdAlter)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
jahre.forEach { j ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(j) },
|
||||
onClick = { pferdAlter = j; expanded = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kontakt
|
||||
item {
|
||||
Text("Kontakt", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("E-Mail Adresse *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = email.isNotBlank() && !isEmailValid
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = telefon,
|
||||
onValueChange = { telefon = it },
|
||||
label = { Text("Telefonnummer (optional)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
// Bewerbe
|
||||
item {
|
||||
Text("Bewerbe / Prüfungen * (Mind. 1 wählen)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
NennungMockData.bewerbe.forEach { bewerb ->
|
||||
val isSelected = ausgewaehlteBewerbe.any { it.nr == bewerb.nr }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
if (isSelected) {
|
||||
val item = ausgewaehlteBewerbe.find { it.nr == bewerb.nr }
|
||||
if (item != null) ausgewaehlteBewerbe.remove(item)
|
||||
} else {
|
||||
ausgewaehlteBewerbe.add(bewerb)
|
||||
}
|
||||
}.padding(vertical = 4.dp)
|
||||
) {
|
||||
Checkbox(checked = isSelected, onCheckedChange = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Bewerb ${bewerb.nr}: ${bewerb.name} (${bewerb.tag})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wünsche
|
||||
item {
|
||||
OutlinedTextField(
|
||||
value = bemerkungen,
|
||||
onValueChange = { bemerkungen = it },
|
||||
label = { Text("Bemerkungen / Wünsche") },
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||
maxLines = 4
|
||||
)
|
||||
}
|
||||
|
||||
// DSGVO
|
||||
item {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = dsgvoAkzeptiert, onCheckedChange = { dsgvoAkzeptiert = it })
|
||||
Text(
|
||||
"Ich stimme zu, dass meine Daten zum Zweck der Nennung verarbeitet werden.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(vertical = 16.dp)) {
|
||||
OutlinedButton(onClick = onBack, modifier = Modifier.weight(1f)) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
onNennenAbgeschickt(
|
||||
NennungPayload(
|
||||
vorname, nachname, lizenz, pferdName, pferdAlter,
|
||||
email, telefon, ausgewaehlteBewerbe.toList(), bemerkungen
|
||||
)
|
||||
)
|
||||
},
|
||||
enabled = canSubmit,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = AppColors.Success)
|
||||
) {
|
||||
Text("Jetzt Nennen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NennungPayload(
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferdName: String,
|
||||
val pferdAlter: String,
|
||||
val email: String,
|
||||
val telefon: String,
|
||||
val bewerbe: List<Bewerb>,
|
||||
val bemerkungen: String
|
||||
)
|
||||
+34
-4
@@ -15,6 +15,8 @@ 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.presentation.web.NennungPayload
|
||||
import at.mocode.frontend.features.nennung.presentation.web.OnlineNennungFormular
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -44,10 +46,17 @@ fun WebMainScreen() {
|
||||
currentScreen = WebScreen.Nennung(vId, tId)
|
||||
}
|
||||
)
|
||||
is WebScreen.Nennung -> NennungWebFormular(
|
||||
veranstaltungId = screen.veranstaltungId,
|
||||
turnierId = screen.turnierId,
|
||||
billingViewModel = billingViewModel,
|
||||
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)
|
||||
},
|
||||
onBack = { currentScreen = WebScreen.Landing }
|
||||
)
|
||||
is WebScreen.Erfolg -> Erfolgsscreen(
|
||||
email = screen.email,
|
||||
onBack = { currentScreen = WebScreen.Landing }
|
||||
)
|
||||
}
|
||||
@@ -58,6 +67,27 @@ fun WebMainScreen() {
|
||||
sealed class WebScreen {
|
||||
data object Landing : WebScreen()
|
||||
data class Nennung(val veranstaltungId: Long, val turnierId: Long) : WebScreen()
|
||||
data class Erfolg(val email: String) : WebScreen()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Erfolgsscreen(email: String, onBack: () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = AppColors.PrimaryContainer),
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Nennung erfolgreich eingegangen!", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Eine Bestätigungsmail wurde an $email gesendet.", style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Button(onClick = onBack) {
|
||||
Text("Zurück zur Startseite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user