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:
-2
@@ -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
|
||||
|
||||
+124
-25
@@ -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")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user