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 {
|
dependencies {
|
||||||
// Interne Module
|
// Interne Module
|
||||||
|
implementation(platform(projects.platform.platformBom))
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
@@ -20,7 +21,20 @@ dependencies {
|
|||||||
implementation(libs.spring.boot.starter.validation)
|
implementation(libs.spring.boot.starter.validation)
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
implementation(libs.spring.boot.starter.mail)
|
implementation(libs.spring.boot.starter.mail)
|
||||||
|
implementation(libs.spring.boot.starter.jdbc)
|
||||||
implementation(libs.jackson.module.kotlin)
|
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.spring.cloud.starter.consul.discovery)
|
||||||
implementation(libs.micrometer.tracing.bridge.brave)
|
implementation(libs.micrometer.tracing.bridge.brave)
|
||||||
implementation(libs.zipkin.reporter.brave)
|
implementation(libs.zipkin.reporter.brave)
|
||||||
|
|||||||
+53
-2
@@ -1,22 +1,36 @@
|
|||||||
package at.mocode.mail.service
|
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.Flags
|
||||||
import jakarta.mail.Folder
|
import jakarta.mail.Folder
|
||||||
import jakarta.mail.Session
|
import jakarta.mail.Session
|
||||||
import jakarta.mail.internet.InternetAddress
|
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.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
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.SimpleMailMessage
|
||||||
import org.springframework.mail.javamail.JavaMailSender
|
import org.springframework.mail.javamail.JavaMailSender
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@Service
|
@Service
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
class MailPollingService(
|
class MailPollingService(
|
||||||
private val mailSender: JavaMailSender,
|
private val mailSender: JavaMailSender,
|
||||||
|
private val nennungRepository: NennungRepository,
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
@Value("\${spring.mail.host}") private val imapHost: String,
|
@Value("\${spring.mail.host}") private val imapHost: String,
|
||||||
@Value("\${spring.mail.port}") private val imapPort: Int,
|
@Value("\${spring.mail.port}") private val imapPort: Int,
|
||||||
@Value("\${spring.mail.username}") private val username: String,
|
@Value("\${spring.mail.username}") private val username: String,
|
||||||
@@ -24,6 +38,15 @@ class MailPollingService(
|
|||||||
) {
|
) {
|
||||||
private val logger = LoggerFactory.getLogger(MailPollingService::class.java)
|
private val logger = LoggerFactory.getLogger(MailPollingService::class.java)
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent::class)
|
||||||
|
@Transactional
|
||||||
|
fun initSchema() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(NennungTable)
|
||||||
|
}
|
||||||
|
logger.info("Datenbankschema für Mail-Service initialisiert.")
|
||||||
|
}
|
||||||
|
|
||||||
@Scheduled(fixedDelay = 60000) // Alle 60 Sekunden pollen
|
@Scheduled(fixedDelay = 60000) // Alle 60 Sekunden pollen
|
||||||
fun pollMails() {
|
fun pollMails() {
|
||||||
if (password.isBlank()) {
|
if (password.isBlank()) {
|
||||||
@@ -61,10 +84,33 @@ class MailPollingService(
|
|||||||
|
|
||||||
if (turnierNr != null) {
|
if (turnierNr != null) {
|
||||||
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
logger.info("Nennung für Turnier $turnierNr erkannt.")
|
||||||
// TODO: Payload parsen und in DB speichern (Tenant-Routing)
|
|
||||||
|
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
|
// Auto-Reply senden
|
||||||
sendAutoReply(message.from?.firstOrNull()?.toString() ?: "", turnierNr)
|
sendAutoReply(entity.email, turnierNr)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Fehler beim Parsen/Speichern der Nennung: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
// Mail als gelesen markieren
|
// Mail als gelesen markieren
|
||||||
message.setFlag(Flags.Flag.SEEN, true)
|
message.setFlag(Flags.Flag.SEEN, true)
|
||||||
@@ -87,6 +133,11 @@ class MailPollingService(
|
|||||||
return match?.groupValues?.get(1)
|
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) {
|
private fun sendAutoReply(to: String, turnierNr: String) {
|
||||||
try {
|
try {
|
||||||
val message = SimpleMailMessage()
|
val message = SimpleMailMessage()
|
||||||
|
|||||||
+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:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: mail-service
|
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:
|
mail:
|
||||||
host: ${MAIL_HOST:imap.world4you.com}
|
host: ${MAIL_HOST:imap.world4you.com}
|
||||||
port: ${MAIL_PORT:993}
|
port: ${MAIL_PORT:993}
|
||||||
|
|||||||
+197
-107
@@ -1,18 +1,37 @@
|
|||||||
package at.mocode.frontend.features.nennung.presentation.web
|
package at.mocode.frontend.features.nennung.presentation.web
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||||
import at.mocode.frontend.features.nennung.domain.Bewerb
|
import at.mocode.frontend.features.nennung.domain.Bewerb
|
||||||
import at.mocode.frontend.features.nennung.domain.NennungMockData
|
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
|
@Composable
|
||||||
fun OnlineNennungFormular(
|
fun OnlineNennungFormular(
|
||||||
turnierNr: String,
|
turnierNr: String,
|
||||||
@@ -42,152 +61,126 @@ fun OnlineNennungFormular(
|
|||||||
ausgewaehlteBewerbe.isNotEmpty() &&
|
ausgewaehlteBewerbe.isNotEmpty() &&
|
||||||
dsgvoAkzeptiert
|
dsgvoAkzeptiert
|
||||||
|
|
||||||
|
// Clean-White Layout: Hintergrund hellgrau, Formular in weißen Cards
|
||||||
|
Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF8F9FA))) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Text("Online-Nennung für Turnier $turnierNr", style = MaterialTheme.typography.headlineSmall)
|
Spacer(Modifier.height(32.dp))
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
Text(
|
||||||
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reiter Daten
|
// --- REITER CARD ---
|
||||||
item {
|
item {
|
||||||
Text("Reiter", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
FormCard("Persönliche Daten (Reiter)") {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(
|
ModernTextField(vorname, { vorname = it }, "Vorname *", Modifier.weight(1f))
|
||||||
value = vorname,
|
ModernTextField(nachname, { nachname = it }, "Nachname *", Modifier.weight(1f))
|
||||||
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("Lizenzklasse", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
Text("Lizenz", style = MaterialTheme.typography.titleSmall)
|
DropdownSelector(lizenz, lizenzen) { lizenz = it }
|
||||||
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
|
// --- PFERD CARD ---
|
||||||
item {
|
item {
|
||||||
Text("Pferd", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
FormCard("Pferdedaten") {
|
||||||
OutlinedTextField(
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
value = pferdName,
|
ModernTextField(pferdName, { pferdName = it }, "Name oder Kopfnummer *")
|
||||||
onValueChange = { pferdName = it },
|
|
||||||
label = { Text("Pferd-Name oder Kopfnummer *") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
Text("Geburtsjahr", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
Text("Geburtsjahr Pferd", style = MaterialTheme.typography.titleSmall)
|
DropdownSelector(pferdAlter, jahre) { pferdAlter = it }
|
||||||
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
|
// --- KONTAKT CARD ---
|
||||||
item {
|
item {
|
||||||
Text("Kontakt", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
FormCard("Kontakt für Rückfragen") {
|
||||||
OutlinedTextField(
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
ModernTextField(
|
||||||
value = email,
|
value = email,
|
||||||
onValueChange = { email = it },
|
onValueChange = { email = it },
|
||||||
label = { Text("E-Mail Adresse *") },
|
label = "E-Mail Adresse *",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
isError = email.isNotBlank() && !isEmailValid
|
isError = email.isNotBlank() && !isEmailValid
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
ModernTextField(telefon, { telefon = it }, "Telefonnummer (optional)")
|
||||||
OutlinedTextField(
|
}
|
||||||
value = telefon,
|
}
|
||||||
onValueChange = { telefon = it },
|
|
||||||
label = { Text("Telefonnummer (optional)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bewerbe
|
// --- BEWERBE CARD ---
|
||||||
item {
|
item {
|
||||||
Text("Bewerbe / Prüfungen * (Mind. 1 wählen)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
FormCard("Bewerbe & Prüfungen") {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
NennungMockData.bewerbe.forEach { bewerb ->
|
NennungMockData.bewerbe.forEach { bewerb ->
|
||||||
val isSelected = ausgewaehlteBewerbe.any { it.nr == bewerb.nr }
|
val isSelected = ausgewaehlteBewerbe.any { it.nr == bewerb.nr }
|
||||||
Row(
|
BewerbRow(bewerb, isSelected) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth().clickable {
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
val item = ausgewaehlteBewerbe.find { it.nr == bewerb.nr }
|
val item = ausgewaehlteBewerbe.find { it.nr == bewerb.nr }
|
||||||
if (item != null) ausgewaehlteBewerbe.remove(item)
|
if (item != null) ausgewaehlteBewerbe.remove(item)
|
||||||
} else {
|
} else {
|
||||||
ausgewaehlteBewerbe.add(bewerb)
|
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
|
// --- WÜNSCHE CARD ---
|
||||||
item {
|
item {
|
||||||
|
FormCard("Anmerkungen") {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = bemerkungen,
|
value = bemerkungen,
|
||||||
onValueChange = { bemerkungen = it },
|
onValueChange = { bemerkungen = it },
|
||||||
label = { Text("Bemerkungen / Wünsche") },
|
placeholder = { Text("Besondere Wünsche, Stallplaketten, etc.") },
|
||||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||||
maxLines = 4
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = AppColors.Primary,
|
||||||
|
unfocusedBorderColor = Color(0xFFE0E0E0)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DSGVO
|
// --- DSGVO & ABSCHLUSS ---
|
||||||
item {
|
item {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.clickable { dsgvoAkzeptiert = !dsgvoAkzeptiert }.padding(8.dp)
|
||||||
|
) {
|
||||||
Checkbox(checked = dsgvoAkzeptiert, onCheckedChange = { dsgvoAkzeptiert = it })
|
Checkbox(checked = dsgvoAkzeptiert, onCheckedChange = { dsgvoAkzeptiert = it })
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
"Ich stimme zu, dass meine Daten zum Zweck der Nennung verarbeitet werden.",
|
"Ich akzeptiere die Datenschutzbestimmungen.",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodyMedium
|
||||||
modifier = Modifier.padding(start = 8.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons
|
Spacer(Modifier.height(16.dp))
|
||||||
item {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(vertical = 16.dp)) {
|
|
||||||
OutlinedButton(onClick = onBack, modifier = Modifier.weight(1f)) {
|
|
||||||
Text("Abbrechen")
|
|
||||||
}
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
onNennenAbgeschickt(
|
onNennenAbgeschickt(
|
||||||
@@ -198,24 +191,121 @@ fun OnlineNennungFormular(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
enabled = canSubmit,
|
enabled = canSubmit,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = AppColors.Success)
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (canSubmit) Color(0xFF2ECC71) else Color(0xFFBDC3C7)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text("Jetzt Nennen")
|
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(
|
@Composable
|
||||||
val vorname: String,
|
fun FormCard(title: String, content: @Composable () -> Unit) {
|
||||||
val nachname: String,
|
Card(
|
||||||
val lizenz: String,
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
val pferdName: String,
|
shape = RoundedCornerShape(20.dp),
|
||||||
val pferdAlter: String,
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
val email: String,
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
val telefon: String,
|
) {
|
||||||
val bewerbe: List<Bewerb>,
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
val bemerkungen: String
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.window.singleWindowApplication
|
import androidx.compose.ui.window.singleWindowApplication
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import at.mocode.desktop.v2.NennungsEingangScreen
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hot-Reload Preview Entry Point
|
* Hot-Reload Preview Entry Point
|
||||||
|
|||||||
+122
-23
@@ -4,11 +4,13 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Email
|
import androidx.compose.material.icons.filled.Email
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
data class OnlineNennungMail(
|
data class OnlineNennungMail(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -24,18 +27,55 @@ data class OnlineNennungMail(
|
|||||||
val empfaenger: String,
|
val empfaenger: String,
|
||||||
val datum: String,
|
val datum: String,
|
||||||
val turnierNr: String,
|
val turnierNr: String,
|
||||||
val reiter: String,
|
val vorname: String,
|
||||||
|
val nachname: String,
|
||||||
|
val lizenz: String,
|
||||||
val pferd: String,
|
val pferd: String,
|
||||||
|
val pferdAlter: String,
|
||||||
|
val telefon: String?,
|
||||||
val bewerbe: String,
|
val bewerbe: String,
|
||||||
val status: String = "Neu"
|
val bemerkungen: String?,
|
||||||
|
var status: String = "NEU"
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NennungsEingangScreen(onBack: () -> Unit) {
|
fun NennungsEingangScreen(onBack: () -> Unit) {
|
||||||
DesktopThemeV2 {
|
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) }
|
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)) {
|
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
// Header
|
// Header
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
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)
|
Icon(Icons.Default.Email, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||||
Text("Nennungs-Eingang (Online-Nennen)", style = MaterialTheme.typography.headlineMedium)
|
Text("Nennungs-Eingang (Online-Nennen)", style = MaterialTheme.typography.headlineMedium)
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||||
Button(
|
Button(
|
||||||
onClick = { /* Refresh Logik */ },
|
onClick = { /* Refresh Logik */ },
|
||||||
enabled = !isRefreshing
|
enabled = !isRefreshing
|
||||||
@@ -54,11 +95,22 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
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,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = Color.Gray
|
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
|
// Tabelle
|
||||||
Card(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
Card(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
||||||
Column {
|
Column {
|
||||||
@@ -67,43 +119,50 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant).padding(12.dp),
|
Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant).padding(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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("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("Reiter", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||||
Text("Pferd", 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("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()
|
HorizontalDivider()
|
||||||
|
|
||||||
if (mails.isEmpty()) {
|
if (filteredMails.isEmpty() && !isRefreshing) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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 {
|
} else {
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
items(mails) { mail ->
|
items(filteredMails) { mail ->
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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.datum, Modifier.width(150.dp), fontSize = 13.sp)
|
||||||
Text(mail.turnierNr, Modifier.width(100.dp), fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
Text(mail.turnierNr, Modifier.width(80.dp), fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||||
Text(mail.reiter, Modifier.width(200.dp), fontSize = 13.sp)
|
Text("${mail.vorname} ${mail.nachname}", Modifier.width(200.dp), fontSize = 13.sp)
|
||||||
Text(mail.pferd, 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)
|
Text(mail.bewerbe, Modifier.weight(1f), fontSize = 13.sp)
|
||||||
|
|
||||||
Row(Modifier.width(150.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Button(
|
Button(
|
||||||
onClick = { /* Übernahme Logik */ },
|
onClick = { selectedMail = mail },
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||||
modifier = Modifier.height(32.dp),
|
modifier = Modifier.width(120.dp).height(32.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
|
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Check, null, modifier = Modifier.size(14.dp))
|
Text("Anzeigen", fontSize = 11.sp)
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
Text("Übernehmen", fontSize = 11.sp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(Modifier.padding(horizontal = 8.dp), thickness = 0.5.dp)
|
HorizontalDivider(Modifier.padding(horizontal = 8.dp), thickness = 0.5.dp)
|
||||||
@@ -116,8 +175,48 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMockMails() = listOf(
|
@Composable
|
||||||
OnlineNennungMail("1", "max.mustermann@web.de", "meldestelle-26128@mo-code.at", "14.04.2026 14:30", "26128", "Max Mustermann", "Spirit", "1, 2, 5"),
|
fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkProcessed: () -> Unit) {
|
||||||
OnlineNennungMail("2", "susi.sorglos@gmx.at", "meldestelle-26128@mo-code.at", "14.04.2026 15:12", "26128", "Susi Sorglos", "Flocke", "10, 11"),
|
AlertDialog(
|
||||||
OnlineNennungMail("3", "info@reitstall-hofer.at", "meldestelle-26129@mo-code.at", "14.04.2026 16:05", "26129", "Georg Hofer", "Black Beauty", "3, 4, 8")
|
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", "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