feat(mail-service): introduce persistence and REST support for Nennungen

- Added `NennungRepository` with methods for saving, updating status, and retrieving entries.
- Created `NennungController` to expose REST endpoints for Nennungen.
- Defined `NennungTable` schema with relevant fields and indices.
- Extended `MailPollingService` to parse incoming emails into `NennungEntity` and persist them.
- Updated `build.gradle.kts` with database dependencies and H2 configuration for local dev.
- Refined frontend layout in `OnlineNennungFormular` for improved usability and responsiveness.
This commit is contained in:
2026-04-14 16:50:24 +02:00
parent 4de44623c2
commit da3b57a91d
10 changed files with 662 additions and 257 deletions
@@ -5,8 +5,6 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.singleWindowApplication
import androidx.lifecycle.viewmodel.compose.viewModel
import at.mocode.desktop.v2.NennungsEingangScreen
/**
* Hot-Reload Preview Entry Point
@@ -4,11 +4,13 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -17,6 +19,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
data class OnlineNennungMail(
val id: String,
@@ -24,18 +27,55 @@ data class OnlineNennungMail(
val empfaenger: String,
val datum: String,
val turnierNr: String,
val reiter: String,
val vorname: String,
val nachname: String,
val lizenz: String,
val pferd: String,
val pferdAlter: String,
val telefon: String?,
val bewerbe: String,
val status: String = "Neu"
val bemerkungen: String?,
var status: String = "NEU"
)
@Composable
fun NennungsEingangScreen(onBack: () -> Unit) {
DesktopThemeV2 {
var mails by remember { mutableStateOf(getMockMails()) }
var mails by remember { mutableStateOf<List<OnlineNennungMail>>(emptyList()) }
var searchQuery by remember { mutableStateOf("") }
var selectedMail by remember { mutableStateOf<OnlineNennungMail?>(null) }
var isRefreshing by remember { mutableStateOf(false) }
val filteredMails = remember(mails, searchQuery) {
if (searchQuery.isBlank()) mails
else mails.filter {
it.vorname.contains(searchQuery, ignoreCase = true) ||
it.nachname.contains(searchQuery, ignoreCase = true) ||
it.pferd.contains(searchQuery, ignoreCase = true) ||
it.turnierNr.contains(searchQuery, ignoreCase = true)
}
}
// Initiales Laden
LaunchedEffect(Unit) {
isRefreshing = true
delay(800)
mails = getMockMails()
isRefreshing = false
}
if (selectedMail != null) {
NennungDetailDialog(
mail = selectedMail!!,
onDismiss = { selectedMail = null },
onMarkProcessed = {
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
mails = updated
selectedMail = null
}
)
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
// Header
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
@@ -43,6 +83,7 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
Icon(Icons.Default.Email, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
Text("Nennungs-Eingang (Online-Nennen)", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.weight(1f))
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
Button(
onClick = { /* Refresh Logik */ },
enabled = !isRefreshing
@@ -54,11 +95,22 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
}
Text(
"Hier werden alle eingegangenen Online-Nennungen angezeigt, die über das Web-Formular an meldestelle-[Nr]@mo-code.at gesendet wurden.",
"Hier werden alle eingegangenen Online-Nennungen angezeigt. Klicke auf 'Anzeigen', um alle Details für die manuelle Übernahme zu sehen.",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
// Suchfeld
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Suche nach Reiter, Pferd oder Turnier-Nr...") },
leadingIcon = { Icon(Icons.Default.Search, null) },
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
// Tabelle
Card(modifier = Modifier.fillMaxWidth().weight(1f)) {
Column {
@@ -67,43 +119,50 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant).padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("Status", Modifier.width(80.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Datum", Modifier.width(150.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Turnier", Modifier.width(100.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Turnier", Modifier.width(80.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Reiter", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Pferd", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Bewerbe", Modifier.weight(1f), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Aktion", Modifier.width(150.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Aktion", Modifier.width(120.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
}
HorizontalDivider()
if (mails.isEmpty()) {
if (filteredMails.isEmpty() && !isRefreshing) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Keine neuen Nennungen vorhanden.", color = Color.Gray)
Text(
if (searchQuery.isBlank()) "Keine neuen Nennungen vorhanden."
else "Keine Nennungen für '$searchQuery' gefunden.",
color = Color.Gray
)
}
} else {
LazyColumn(Modifier.fillMaxSize()) {
items(mails) { mail ->
items(filteredMails) { mail ->
Row(
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Badge(
containerColor = if (mail.status == "NEU") Color(0xFFE74C3C) else Color(0xFFBDC3C7),
modifier = Modifier.width(80.dp).padding(end = 8.dp)
) {
Text(mail.status, color = Color.White, fontSize = 10.sp)
}
Text(mail.datum, Modifier.width(150.dp), fontSize = 13.sp)
Text(mail.turnierNr, Modifier.width(100.dp), fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Text(mail.reiter, Modifier.width(200.dp), fontSize = 13.sp)
Text(mail.turnierNr, Modifier.width(80.dp), fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Text("${mail.vorname} ${mail.nachname}", Modifier.width(200.dp), fontSize = 13.sp)
Text(mail.pferd, Modifier.width(200.dp), fontSize = 13.sp)
Text(mail.bewerbe, Modifier.weight(1f), fontSize = 13.sp)
Row(Modifier.width(150.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { /* Übernahme Logik */ },
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
modifier = Modifier.height(32.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Icon(Icons.Default.Check, null, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(4.dp))
Text("Übernehmen", fontSize = 11.sp)
}
Button(
onClick = { selectedMail = mail },
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
modifier = Modifier.width(120.dp).height(32.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Text("Anzeigen", fontSize = 11.sp)
}
}
HorizontalDivider(Modifier.padding(horizontal = 8.dp), thickness = 0.5.dp)
@@ -116,8 +175,48 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
}
}
@Composable
fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkProcessed: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Details zur Online-Nennung") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
DetailRow("Absender", mail.sender)
DetailRow("Turnier", mail.turnierNr)
DetailRow("Eingang", mail.datum)
HorizontalDivider()
Text("Reiter: ${mail.vorname} ${mail.nachname} (${mail.lizenz})", fontWeight = FontWeight.Bold)
Text("Pferd: ${mail.pferd} (Geb. ${mail.pferdAlter})", fontWeight = FontWeight.Bold)
DetailRow("Telefon", mail.telefon ?: "-")
HorizontalDivider()
Text("Ausgewählte Bewerbe:", fontWeight = FontWeight.SemiBold)
Text(mail.bewerbe)
if (!mail.bemerkungen.isNullOrBlank()) {
Text("Bemerkungen:", fontWeight = FontWeight.SemiBold)
Text(mail.bemerkungen, color = Color.DarkGray)
}
}
},
confirmButton = {
Button(onClick = onMarkProcessed) { Text("Als gelesen markieren") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Schließen") }
}
)
}
@Composable
fun DetailRow(label: String, value: String) {
Row(Modifier.fillMaxWidth()) {
Text("$label: ", fontWeight = FontWeight.SemiBold, modifier = Modifier.width(100.dp))
Text(value)
}
}
private fun getMockMails() = listOf(
OnlineNennungMail("1", "max.mustermann@web.de", "meldestelle-26128@mo-code.at", "14.04.2026 14:30", "26128", "Max Mustermann", "Spirit", "1, 2, 5"),
OnlineNennungMail("2", "susi.sorglos@gmx.at", "meldestelle-26128@mo-code.at", "14.04.2026 15:12", "26128", "Susi Sorglos", "Flocke", "10, 11"),
OnlineNennungMail("3", "info@reitstall-hofer.at", "meldestelle-26129@mo-code.at", "14.04.2026 16:05", "26129", "Georg Hofer", "Black Beauty", "3, 4, 8")
OnlineNennungMail("1", "max.mustermann@web.de", "meldestelle-26128@mo-code.at", "14.04.2026 14:30", "26128", "Max", "Mustermann", "R2", "Spirit", "2015", "0664/1234567", "1, 2, 5", "Brauche Box für Freitag"),
OnlineNennungMail("2", "susi.sorglos@gmx.at", "meldestelle-26128@mo-code.at", "14.04.2026 15:12", "26128", "Susi", "Sorglos", "LF", "Flocke", "2018", null, "10, 11", null),
OnlineNennungMail("3", "info@reitstall-hofer.at", "meldestelle-26129@mo-code.at", "14.04.2026 16:05", "26129", "Georg", "Hofer", "R3", "Black Beauty", "2012", "0676/9876543", "3, 4, 8", "Bitte späte Startzeit")
)