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:
@@ -11,6 +11,7 @@ springBoot {
|
||||
|
||||
dependencies {
|
||||
// Interne Module
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
@@ -20,7 +21,20 @@ dependencies {
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.actuator)
|
||||
implementation(libs.spring.boot.starter.mail)
|
||||
implementation(libs.spring.boot.starter.jdbc)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.jackson.datatype.jsr310)
|
||||
|
||||
// Database & Exposed
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.java.time)
|
||||
implementation(libs.exposed.json)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.h2.driver)
|
||||
implementation(libs.postgresql.driver)
|
||||
implementation(libs.hikari.cp)
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
implementation(libs.micrometer.tracing.bridge.brave)
|
||||
implementation(libs.zipkin.reporter.brave)
|
||||
|
||||
+116
-65
@@ -1,99 +1,150 @@
|
||||
package at.mocode.mail.service
|
||||
|
||||
import at.mocode.mail.service.persistence.NennungEntity
|
||||
import at.mocode.mail.service.persistence.NennungRepository
|
||||
import at.mocode.mail.service.persistence.NennungTable
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.mail.Flags
|
||||
import jakarta.mail.Folder
|
||||
import jakarta.mail.Session
|
||||
import jakarta.mail.internet.InternetAddress
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
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.*
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@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 mailSender: JavaMailSender,
|
||||
private val nennungRepository: NennungRepository,
|
||||
private val objectMapper: ObjectMapper,
|
||||
@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)
|
||||
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
|
||||
}
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
@Transactional
|
||||
fun initSchema() {
|
||||
transaction {
|
||||
SchemaUtils.create(NennungTable)
|
||||
}
|
||||
logger.info("Datenbankschema für Mail-Service initialisiert.")
|
||||
}
|
||||
|
||||
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"
|
||||
@Scheduled(fixedDelay = 60000) // Alle 60 Sekunden pollen
|
||||
fun pollMails() {
|
||||
if (password.isBlank()) {
|
||||
logger.warn("Mail-Passwort nicht gesetzt. Polling übersprungen.")
|
||||
return
|
||||
}
|
||||
|
||||
val session = Session.getInstance(props)
|
||||
val store = session.getStore("imaps")
|
||||
store.connect(imapHost, username, password)
|
||||
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 inbox = store.getFolder("INBOX")
|
||||
inbox.open(Folder.READ_WRITE)
|
||||
val session = Session.getInstance(props)
|
||||
val store = session.getStore("imaps")
|
||||
store.connect(imapHost, username, password)
|
||||
|
||||
// Nur ungelesene Nachrichten
|
||||
val messages = inbox.getMessages()
|
||||
logger.info("Gefundene Nachrichten in INBOX: ${messages.size}")
|
||||
val inbox = store.getFolder("INBOX")
|
||||
inbox.open(Folder.READ_WRITE)
|
||||
|
||||
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 ?: ""
|
||||
// Nur ungelesene Nachrichten
|
||||
val messages = inbox.getMessages()
|
||||
logger.info("Gefundene Nachrichten in INBOX: ${messages.size}")
|
||||
|
||||
logger.info("Neue Mail empfangen von: ${message.from?.firstOrNull()} an: $toAddress")
|
||||
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 ?: ""
|
||||
|
||||
// Turnier-Nr extrahieren: meldestelle-26128@mo-code.at
|
||||
val turnierNr = extractTurnierNr(toAddress)
|
||||
logger.info("Neue Mail empfangen von: ${message.from?.firstOrNull()} an: $toAddress")
|
||||
|
||||
if (turnierNr != null) {
|
||||
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
||||
// TODO: Payload parsen und in DB speichern (Tenant-Routing)
|
||||
// Turnier-Nr extrahieren: meldestelle-26128@mo-code.at
|
||||
val turnierNr = extractTurnierNr(toAddress)
|
||||
|
||||
// Auto-Reply senden
|
||||
sendAutoReply(message.from?.firstOrNull()?.toString() ?: "", turnierNr)
|
||||
if (turnierNr != null) {
|
||||
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
||||
|
||||
// Mail als gelesen markieren
|
||||
message.setFlag(Flags.Flag.SEEN, true)
|
||||
} else {
|
||||
logger.warn("Keine Turnier-Nr in Adresse $toAddress gefunden. Mail wird ignoriert.")
|
||||
}
|
||||
}
|
||||
try {
|
||||
val content = message.content.toString()
|
||||
|
||||
val entity = NennungEntity(
|
||||
id = Uuid.random(),
|
||||
turnierNr = turnierNr,
|
||||
status = "NEU",
|
||||
vorname = extractValue(content, "Vorname") ?: "Unbekannt",
|
||||
nachname = extractValue(content, "Nachname") ?: "Unbekannt",
|
||||
lizenz = extractValue(content, "Lizenz") ?: "LF",
|
||||
pferdName = extractValue(content, "Pferd") ?: "Unbekannt",
|
||||
pferdAlter = extractValue(content, "Alter") ?: "2020",
|
||||
email = (message.from?.firstOrNull() as? InternetAddress)?.address ?: "unbekannt@test.at",
|
||||
telefon = extractValue(content, "Telefon"),
|
||||
bewerbe = extractValue(content, "Bewerbe") ?: "[]",
|
||||
bemerkungen = extractValue(content, "Bemerkungen")
|
||||
)
|
||||
|
||||
nennungRepository.save(entity)
|
||||
logger.info("Nennung für ${entity.vorname} ${entity.nachname} erfolgreich persistiert.")
|
||||
|
||||
// Auto-Reply senden
|
||||
sendAutoReply(entity.email, turnierNr)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Parsen/Speichern der Nennung: ${e.message}")
|
||||
}
|
||||
|
||||
inbox.close(false)
|
||||
store.close()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Mail-Polling: ${e.message}", e)
|
||||
// Mail als gelesen markieren
|
||||
message.setFlag(Flags.Flag.SEEN, true)
|
||||
} else {
|
||||
logger.warn("Keine Turnier-Nr in Adresse $toAddress gefunden. Mail wird ignoriert.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractTurnierNr(address: String): String? {
|
||||
val regex = Regex("meldestelle-(\\d+)@.*")
|
||||
val match = regex.find(address)
|
||||
return match?.groupValues?.get(1)
|
||||
inbox.close(false)
|
||||
store.close()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Fehler beim Mail-Polling: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
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 = """
|
||||
private fun extractTurnierNr(address: String): String? {
|
||||
val regex = Regex("meldestelle-(\\d+)@.*")
|
||||
val match = regex.find(address)
|
||||
return match?.groupValues?.get(1)
|
||||
}
|
||||
|
||||
private fun extractValue(content: String, key: String): String? {
|
||||
val regex = Regex("$key:\\s*(.*)")
|
||||
return regex.find(content)?.groupValues?.get(1)?.trim()
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -105,10 +156,10 @@ class MailPollingService(
|
||||
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}")
|
||||
}
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ import org.springframework.boot.runApplication
|
||||
class MailServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MailServiceApplication>(*args)
|
||||
runApplication<MailServiceApplication>(*args)
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.mail.service
|
||||
|
||||
import at.mocode.mail.service.persistence.NennungEntity
|
||||
import at.mocode.mail.service.persistence.NennungRepository
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/mail/nennungen")
|
||||
class NennungController(
|
||||
private val nennungRepository: NennungRepository
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun getAllNennungen(): List<NennungEntity> {
|
||||
return nennungRepository.findAll()
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/status")
|
||||
fun updateStatus(
|
||||
@PathVariable id: String,
|
||||
@RequestBody newStatus: String
|
||||
) {
|
||||
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.mail.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
data class NennungEntity(
|
||||
val id: Uuid,
|
||||
val turnierNr: String,
|
||||
val status: 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?
|
||||
)
|
||||
|
||||
@Repository
|
||||
@Transactional
|
||||
class NennungRepository {
|
||||
|
||||
fun save(nennung: NennungEntity) {
|
||||
transaction {
|
||||
NennungTable.insert {
|
||||
it[id] = nennung.id
|
||||
it[turnierNr] = nennung.turnierNr
|
||||
it[status] = nennung.status
|
||||
it[vorname] = nennung.vorname
|
||||
it[nachname] = nennung.nachname
|
||||
it[lizenz] = nennung.lizenz
|
||||
it[pferdName] = nennung.pferdName
|
||||
it[pferdAlter] = nennung.pferdAlter
|
||||
it[email] = nennung.email
|
||||
it[telefon] = nennung.telefon
|
||||
it[bewerbe] = nennung.bewerbe
|
||||
it[bemerkungen] = nennung.bemerkungen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStatus(id: Uuid, newStatus: String) {
|
||||
transaction {
|
||||
NennungTable.update({ NennungTable.id eq id }) {
|
||||
it[status] = newStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findAll(): List<NennungEntity> {
|
||||
return transaction {
|
||||
NennungTable.selectAll().map {
|
||||
NennungEntity(
|
||||
id = it[NennungTable.id],
|
||||
turnierNr = it[NennungTable.turnierNr],
|
||||
status = it[NennungTable.status],
|
||||
vorname = it[NennungTable.vorname],
|
||||
nachname = it[NennungTable.nachname],
|
||||
lizenz = it[NennungTable.lizenz],
|
||||
pferdName = it[NennungTable.pferdName],
|
||||
pferdAlter = it[NennungTable.pferdAlter],
|
||||
email = it[NennungTable.email],
|
||||
telefon = it[NennungTable.telefon],
|
||||
bewerbe = it[NennungTable.bewerbe],
|
||||
bemerkungen = it[NennungTable.bemerkungen]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.mail.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.javatime.CurrentTimestamp
|
||||
import org.jetbrains.exposed.v1.javatime.timestamp
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
object NennungTable : Table("nennungen") {
|
||||
val id = uuid("id")
|
||||
val turnierNr = varchar("turnier_nr", 20)
|
||||
val status = varchar("status", 20) // NEU, GELESEN, UEBERNOMMEN
|
||||
val eingangsdatum = timestamp("eingangsdatum").defaultExpression(CurrentTimestamp)
|
||||
|
||||
// Reiter Daten
|
||||
val vorname = varchar("vorname", 100)
|
||||
val nachname = varchar("nachname", 100)
|
||||
val lizenz = varchar("lizenz", 50)
|
||||
|
||||
// Pferd Daten
|
||||
val pferdName = varchar("pferd_name", 100)
|
||||
val pferdAlter = varchar("pferd_alter", 10)
|
||||
|
||||
// Kontakt
|
||||
val email = varchar("email", 150)
|
||||
val telefon = varchar("telefon", 50).nullable()
|
||||
|
||||
// Payload (Bewerbe & Bemerkungen)
|
||||
val bewerbe = text("bewerbe") // Kommagetrennte Liste der Bewerbs-Nummern
|
||||
val bemerkungen = text("bemerkungen").nullable()
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
spring:
|
||||
application:
|
||||
name: mail-service
|
||||
datasource:
|
||||
url: jdbc:h2:mem:maildb;DB_CLOSE_DELAY=-1
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password: ""
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
mail:
|
||||
host: ${MAIL_HOST:imap.world4you.com}
|
||||
port: ${MAIL_PORT:993}
|
||||
|
||||
+254
-164
@@ -1,18 +1,37 @@
|
||||
package at.mocode.frontend.features.nennung.presentation.web
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.features.nennung.domain.Bewerb
|
||||
import at.mocode.frontend.features.nennung.domain.NennungMockData
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun OnlineNennungFormular(
|
||||
turnierNr: String,
|
||||
@@ -42,180 +61,251 @@ fun OnlineNennungFormular(
|
||||
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 })
|
||||
// Clean-White Layout: Hintergrund hellgrau, Formular in weißen Cards
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF8F9FA))) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Text(
|
||||
"Ich stimme zu, dass meine Daten zum Zweck der Nennung verarbeitet werden.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
text = "Turnier Online-Nennung",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color(0xFF2D3436)
|
||||
)
|
||||
Text(
|
||||
text = "Turnier-Nr: $turnierNr",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(vertical = 16.dp)) {
|
||||
OutlinedButton(onClick = onBack, modifier = Modifier.weight(1f)) {
|
||||
Text("Abbrechen")
|
||||
// --- REITER CARD ---
|
||||
item {
|
||||
FormCard("Persönliche Daten (Reiter)") {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ModernTextField(vorname, { vorname = it }, "Vorname *", Modifier.weight(1f))
|
||||
ModernTextField(nachname, { nachname = it }, "Nachname *", Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Text("Lizenzklasse", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||
DropdownSelector(lizenz, lizenzen) { lizenz = it }
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
onNennenAbgeschickt(
|
||||
NennungPayload(
|
||||
vorname, nachname, lizenz, pferdName, pferdAlter,
|
||||
email, telefon, ausgewaehlteBewerbe.toList(), bemerkungen
|
||||
)
|
||||
}
|
||||
|
||||
// --- PFERD CARD ---
|
||||
item {
|
||||
FormCard("Pferdedaten") {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ModernTextField(pferdName, { pferdName = it }, "Name oder Kopfnummer *")
|
||||
|
||||
Text("Geburtsjahr", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||
DropdownSelector(pferdAlter, jahre) { pferdAlter = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- KONTAKT CARD ---
|
||||
item {
|
||||
FormCard("Kontakt für Rückfragen") {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ModernTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = "E-Mail Adresse *",
|
||||
isError = email.isNotBlank() && !isEmailValid
|
||||
)
|
||||
},
|
||||
enabled = canSubmit,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = AppColors.Success)
|
||||
ModernTextField(telefon, { telefon = it }, "Telefonnummer (optional)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- BEWERBE CARD ---
|
||||
item {
|
||||
FormCard("Bewerbe & Prüfungen") {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NennungMockData.bewerbe.forEach { bewerb ->
|
||||
val isSelected = ausgewaehlteBewerbe.any { it.nr == bewerb.nr }
|
||||
BewerbRow(bewerb, isSelected) {
|
||||
if (isSelected) {
|
||||
val item = ausgewaehlteBewerbe.find { it.nr == bewerb.nr }
|
||||
if (item != null) ausgewaehlteBewerbe.remove(item)
|
||||
} else {
|
||||
ausgewaehlteBewerbe.add(bewerb)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- WÜNSCHE CARD ---
|
||||
item {
|
||||
FormCard("Anmerkungen") {
|
||||
OutlinedTextField(
|
||||
value = bemerkungen,
|
||||
onValueChange = { bemerkungen = it },
|
||||
placeholder = { Text("Besondere Wünsche, Stallplaketten, etc.") },
|
||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
unfocusedBorderColor = Color(0xFFE0E0E0)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DSGVO & ABSCHLUSS ---
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("Jetzt Nennen")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { dsgvoAkzeptiert = !dsgvoAkzeptiert }.padding(8.dp)
|
||||
) {
|
||||
Checkbox(checked = dsgvoAkzeptiert, onCheckedChange = { dsgvoAkzeptiert = it })
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Ich akzeptiere die Datenschutzbestimmungen.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onNennenAbgeschickt(
|
||||
NennungPayload(
|
||||
vorname, nachname, lizenz, pferdName, pferdAlter,
|
||||
email, telefon, ausgewaehlteBewerbe.toList(), bemerkungen
|
||||
)
|
||||
)
|
||||
},
|
||||
enabled = canSubmit,
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (canSubmit) Color(0xFF2ECC71) else Color(0xFFBDC3C7)
|
||||
)
|
||||
) {
|
||||
Text("JETZT NENNEN", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
}
|
||||
|
||||
TextButton(onClick = onBack, modifier = Modifier.padding(top = 8.dp)) {
|
||||
Text("Abbrechen", color = Color.Gray)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
@Composable
|
||||
fun FormCard(title: String, content: @Composable () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.Primary,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModernTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isError: Boolean = false
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
isError = isError,
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
unfocusedBorderColor = Color(0xFFE0E0E0),
|
||||
errorBorderColor = Color.Red
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownSelector(current: String, options: List<String>, onSelect: (String) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Black),
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(brush = androidx.compose.ui.graphics.SolidColor(Color(0xFFE0E0E0)))
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(current)
|
||||
Icon(Icons.Default.Info, null, modifier = Modifier.size(18.dp), tint = Color.LightGray)
|
||||
}
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
options.forEach { opt ->
|
||||
DropdownMenuItem(text = { Text(opt) }, onClick = { onSelect(opt); expanded = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BewerbRow(bewerb: Bewerb, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (isSelected) Color(0xFFE8F5E9) else Color(0xFFF5F5F5),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Checkbox(checked = isSelected, onCheckedChange = null)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
"Bewerb ${bewerb.nr}: ${bewerb.name}",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Text(
|
||||
bewerb.tag,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-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