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:
@@ -0,0 +1,36 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinJvm)
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
|
alias(libs.plugins.kotlinSpring)
|
||||||
|
}
|
||||||
|
|
||||||
|
springBoot {
|
||||||
|
mainClass.set("at.mocode.mail.service.MailServiceApplicationKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Interne Module
|
||||||
|
implementation(projects.platform.platformDependencies)
|
||||||
|
implementation(projects.core.coreUtils)
|
||||||
|
implementation(projects.core.coreDomain)
|
||||||
|
|
||||||
|
// Spring Boot Starters
|
||||||
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
implementation(libs.spring.boot.starter.validation)
|
||||||
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
|
implementation(libs.spring.boot.starter.mail)
|
||||||
|
implementation(libs.jackson.module.kotlin)
|
||||||
|
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||||
|
implementation(libs.micrometer.tracing.bridge.brave)
|
||||||
|
implementation(libs.zipkin.reporter.brave)
|
||||||
|
implementation(libs.zipkin.sender.okhttp3)
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
testImplementation(libs.spring.boot.starter.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package at.mocode.mail.service
|
||||||
|
|
||||||
|
import jakarta.mail.Flags
|
||||||
|
import jakarta.mail.Folder
|
||||||
|
import jakarta.mail.Session
|
||||||
|
import jakarta.mail.internet.InternetAddress
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.mail.SimpleMailMessage
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@EnableScheduling
|
||||||
|
class MailPollingService(
|
||||||
|
private val mailSender: JavaMailSender,
|
||||||
|
@Value("\${spring.mail.host}") private val imapHost: String,
|
||||||
|
@Value("\${spring.mail.port}") private val imapPort: Int,
|
||||||
|
@Value("\${spring.mail.username}") private val username: String,
|
||||||
|
@Value("\${spring.mail.password}") private val password: String
|
||||||
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(MailPollingService::class.java)
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 60000) // Alle 60 Sekunden pollen
|
||||||
|
fun pollMails() {
|
||||||
|
if (password.isBlank()) {
|
||||||
|
logger.warn("Mail-Passwort nicht gesetzt. Polling übersprungen.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val props = Properties()
|
||||||
|
props["mail.store.protocol"] = "imaps"
|
||||||
|
props["mail.imaps.host"] = imapHost
|
||||||
|
props["mail.imaps.port"] = imapPort.toString()
|
||||||
|
props["mail.imaps.ssl.enable"] = "true"
|
||||||
|
|
||||||
|
val session = Session.getInstance(props)
|
||||||
|
val store = session.getStore("imaps")
|
||||||
|
store.connect(imapHost, username, password)
|
||||||
|
|
||||||
|
val inbox = store.getFolder("INBOX")
|
||||||
|
inbox.open(Folder.READ_WRITE)
|
||||||
|
|
||||||
|
// Nur ungelesene Nachrichten
|
||||||
|
val messages = inbox.getMessages()
|
||||||
|
logger.info("Gefundene Nachrichten in INBOX: ${messages.size}")
|
||||||
|
|
||||||
|
for (message in messages) {
|
||||||
|
if (!message.isSet(Flags.Flag.SEEN)) {
|
||||||
|
val recipients = message.getRecipients(jakarta.mail.Message.RecipientType.TO)
|
||||||
|
val toAddress = (recipients?.firstOrNull() as? InternetAddress)?.address ?: ""
|
||||||
|
|
||||||
|
logger.info("Neue Mail empfangen von: ${message.from?.firstOrNull()} an: $toAddress")
|
||||||
|
|
||||||
|
// Turnier-Nr extrahieren: meldestelle-26128@mo-code.at
|
||||||
|
val turnierNr = extractTurnierNr(toAddress)
|
||||||
|
|
||||||
|
if (turnierNr != null) {
|
||||||
|
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
||||||
|
// TODO: Payload parsen und in DB speichern (Tenant-Routing)
|
||||||
|
|
||||||
|
// Auto-Reply senden
|
||||||
|
sendAutoReply(message.from?.firstOrNull()?.toString() ?: "", turnierNr)
|
||||||
|
|
||||||
|
// Mail als gelesen markieren
|
||||||
|
message.setFlag(Flags.Flag.SEEN, true)
|
||||||
|
} else {
|
||||||
|
logger.warn("Keine Turnier-Nr in Adresse $toAddress gefunden. Mail wird ignoriert.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inbox.close(false)
|
||||||
|
store.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Fehler beim Mail-Polling: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractTurnierNr(address: String): String? {
|
||||||
|
val regex = Regex("meldestelle-(\\d+)@.*")
|
||||||
|
val match = regex.find(address)
|
||||||
|
return match?.groupValues?.get(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendAutoReply(to: String, turnierNr: String) {
|
||||||
|
try {
|
||||||
|
val message = SimpleMailMessage()
|
||||||
|
message.from = username
|
||||||
|
message.setTo(to)
|
||||||
|
message.subject = "Eingangsbestätigung: Ihre Nennung für Turnier $turnierNr"
|
||||||
|
message.text = """
|
||||||
|
Sehr geehrte Damen und Herren,
|
||||||
|
|
||||||
|
vielen Dank für Ihre Online-Nennung für das Turnier $turnierNr.
|
||||||
|
|
||||||
|
Ihre Nennung ist erfolgreich in unserem System eingegangen und wird nun von der Meldestelle geprüft.
|
||||||
|
Sobald die Nennung final verarbeitet wurde, erhalten Sie eine weitere Bestätigung.
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen,
|
||||||
|
Ihre Turniermeldestelle
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mailSender.send(message)
|
||||||
|
logger.info("Auto-Reply an $to für Turnier $turnierNr gesendet.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Fehler beim Senden des Auto-Replies: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package at.mocode.mail.service
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class MailServiceApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<MailServiceApplication>(*args)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: mail-service
|
||||||
|
mail:
|
||||||
|
host: ${MAIL_HOST:imap.world4you.com}
|
||||||
|
port: ${MAIL_PORT:993}
|
||||||
|
username: ${MAIL_USERNAME:online-nennen@mo-code.at}
|
||||||
|
password: ${MAIL_PASSWORD:}
|
||||||
|
properties:
|
||||||
|
mail:
|
||||||
|
store:
|
||||||
|
protocol: imaps
|
||||||
|
imaps:
|
||||||
|
host: ${MAIL_HOST:imap.world4you.com}
|
||||||
|
port: ${MAIL_PORT:993}
|
||||||
|
ssl:
|
||||||
|
enable: true
|
||||||
|
smtp:
|
||||||
|
auth: true
|
||||||
|
starttls:
|
||||||
|
enable: true
|
||||||
|
host-smtp: ${SMTP_HOST:smtp.world4you.com}
|
||||||
|
port-smtp: ${SMTP_PORT:587}
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8085
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: "health,info,prometheus"
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Roadmap: Online-Nennung & Mail-Service (Phase 5)
|
||||||
|
|
||||||
|
## 🏗️ [Lead Architect] | 14. April 2026
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die Umsetzung der Online-Nennung für das Turnier in Neumarkt (24. April 2026).
|
||||||
|
Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Service` sendet, welcher diese verarbeitet und in der Desktop-Zentrale zur manuellen Übernahme bereitstellt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: E-Mail-Infrastruktur (Vorbereitung) ✅
|
||||||
|
* [x] Definition des Adress-Schemas: `meldestelle-[Turnier-Nr]@mo-code.at`.
|
||||||
|
* [x] Konfiguration der World4You SMTP/IMAP Zugangsdaten.
|
||||||
|
* [x] Mailpit Integration für lokale Tests (bereits in `dc-ops.yaml`).
|
||||||
|
|
||||||
|
### Phase 2: Das Web-Formular (WasmJS Frontend) 🏗️
|
||||||
|
* [ ] **Basis-UI:** Erstellung des Formulars gemäß Spezifikation (Reiter, Pferd, Lizenz, Bewerbe).
|
||||||
|
* [ ] **Validierung:** Implementierung der Pflichtfeld-Prüfung (Buttonsperre bis alles ok).
|
||||||
|
* [ ] **Mail-Versand:** Integration des SMTP-Clients (oder API-Call an Backend), um die strukturierte E-Mail zu senden.
|
||||||
|
* [ ] **DSGVO:** Checkbox und Hinweistext einbauen.
|
||||||
|
|
||||||
|
### Phase 3: Mail-Service (Backend-Verarbeitung) 🏗️
|
||||||
|
* [ ] **Polling:** Implementierung des IMAP-Pollers (imap.world4you.com).
|
||||||
|
* [ ] **Parsing:** Extraktion der Turnier-Nummer aus dem `To`-Header und Mapping auf das Datenbank-Schema (Tenant).
|
||||||
|
* [ ] **Auto-Reply:** Automatisches Versenden der Eingangsbestätigung an den Absender.
|
||||||
|
* [ ] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle für den `registration-context`.
|
||||||
|
|
||||||
|
### Phase 4: Desktop-Zentrale Integration 🏗️
|
||||||
|
* [ ] **UI-Tab:** Neuer Reiter "Nennungs-Eingang" in der Turnierverwaltung.
|
||||||
|
* [ ] **Vorschau:** Anzeige der eingegangenen Mails mit Details (Reiter, Pferd, Bewerbe).
|
||||||
|
* [ ] **Übernahme:** "Übernehmen"-Button, der die Daten in die Turnieranmeldung vor-ausfüllt.
|
||||||
|
* [ ] **Abschluss:** Manueller "Bestätigen"-Button zum Versenden der finalen Bestätigungsmail.
|
||||||
|
|
||||||
|
### Phase 5: End-to-End Test & Deployment 🚀 (Deadline: 21.04.2026)
|
||||||
|
* [ ] Test-Nennung über Web-Formular (Mailpit).
|
||||||
|
* [ ] Verifikation der Schema-Zuordnung im Backend.
|
||||||
|
* [ ] Live-Test mit `online-nennen@mo-code.at`.
|
||||||
|
* [ ] Go-Live für Neumarkt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Meilensteine
|
||||||
|
1. **16.04.:** Web-Formular ist funktionsfähig (Senden möglich).
|
||||||
|
2. **18.04.:** Mail-Service verarbeitet Mails und sendet Auto-Antworten.
|
||||||
|
3. **20.04.:** Desktop-UI zur Übernahme ist fertig.
|
||||||
|
4. **24.04.:** Erstes Turnier (Neumarkt) startet mit Online-Nenn-System.
|
||||||
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature-Modul: Nennungs-Maske (Desktop-only)
|
* 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 {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
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 androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
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
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -44,10 +46,17 @@ fun WebMainScreen() {
|
|||||||
currentScreen = WebScreen.Nennung(vId, tId)
|
currentScreen = WebScreen.Nennung(vId, tId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
is WebScreen.Nennung -> NennungWebFormular(
|
is WebScreen.Nennung -> OnlineNennungFormular(
|
||||||
veranstaltungId = screen.veranstaltungId,
|
turnierNr = screen.turnierId.toString(),
|
||||||
turnierId = screen.turnierId,
|
onNennenAbgeschickt = { payload ->
|
||||||
billingViewModel = billingViewModel,
|
// 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 }
|
onBack = { currentScreen = WebScreen.Landing }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -58,6 +67,27 @@ fun WebMainScreen() {
|
|||||||
sealed class WebScreen {
|
sealed class WebScreen {
|
||||||
data object Landing : WebScreen()
|
data object Landing : WebScreen()
|
||||||
data class Nennung(val veranstaltungId: Long, val turnierId: Long) : 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
|
@Composable
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ include(":backend:services:masterdata:masterdata-service")
|
|||||||
include(":backend:services:billing:billing-domain")
|
include(":backend:services:billing:billing-domain")
|
||||||
include(":backend:services:billing:billing-service")
|
include(":backend:services:billing:billing-service")
|
||||||
|
|
||||||
|
// --- MAIL (Mail-Service für Online-Nennungen) ---
|
||||||
|
include(":backend:services:mail:mail-service")
|
||||||
|
|
||||||
// --- PING (Ping Service) ---
|
// --- PING (Ping Service) ---
|
||||||
include(":backend:services:ping:ping-service")
|
include(":backend:services:ping:ping-service")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user