refactor(desktop): Alte Verwaltungsscreens entfernt und Code reduziert

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-17 12:26:15 +02:00
parent 3949ab21db
commit 8857d52f44
15 changed files with 1131 additions and 1362 deletions

View File

@ -0,0 +1,50 @@
# Journal: Desktop-Struktur Reorganisation & V2-Eliminierung
**Datum:** 17. April 2026
**Agent:** 🏗️ [Lead Architect] & 🧹 [Curator]
## 🎯 Zielsetzung
Eliminierung des veralteten `at/mocode/desktop/v2` Verzeichnisses und Überführung der Komponenten in eine logisch
strukturierte Paket-Hierarchie unter `at.mocode.desktop.screens`. Entfernung aller `V2` Suffixe in Funktions- und
Klassennamen.
## 🛠️ Durchgeführte Änderungen
### 1. Dateireorganisation (Verschiebung & Aufteilung)
- **Daten:** `Stores.kt` und der neu extrahierte `TurnierStore.kt` befinden sich nun in `at.mocode.desktop.data`.
- **Theme:** Das globale `DesktopTheme` wurde nach `at.mocode.desktop.theme` verschoben und von `DesktopThemeV2` in
`DesktopTheme` umbenannt.
- **Screens:** Die massiven Screen-Dateien wurden fachlich aufgeteilt:
- `at.mocode.desktop.screens.management`: `ManagementScreens.kt`, `VeranstalterScreens.kt` (extrahiert aus
`Screens.kt`).
- `at.mocode.desktop.screens.onboarding`: `OnboardingScreen.kt` (extrahiert aus `Screens.kt`).
- `at.mocode.desktop.screens.profile`: `ProfileScreens.kt` (enthält nun nur noch die Profil-Ansichten für Reiter,
Pferde, Vereine und Funktionäre).
- `at.mocode.desktop.screens.veranstaltung`: `VeranstaltungScreens.kt`.
- `at.mocode.desktop.screens.nennung`: `NennungsEingangScreen.kt`.
### 2. Namens-Konsolidierung
- Alle Funktionen wurden von ihrem `V2` Suffix befreit (z.B. `PferdProfilV2` -> `PferdProfil`, `VeranstalterDetailV2` ->
`VeranstalterDetail`).
- Ungenutzte Code-Fragmente wurden im Zuge des Refactorings eliminiert.
### 3. Infrastruktur-Updates
- `DesktopMainLayout.kt` wurde vollständig auf die neue Struktur migriert. Alle statischen Pfad-Referenzen auf `v2`
wurden entfernt.
- `main.kt` nutzt nun den korrekten Pfad für den Daten-Seed (`at.mocode.desktop.data.Store.seed()`).
- In `TurnierStammdatenTab.kt` wurde der Reflection-Zugriff auf den `TurnierStore` an die neue Paketstruktur angepasst.
## ✅ Verifikation
- Manuelle Prüfung der Paket-Deklarationen in allen verschobenen Dateien.
- Syntax-Check der Haupt-Layout-Datei `DesktopMainLayout.kt`.
- Der Ordner `at/mocode/desktop/v2` wurde physisch vom Dateisystem entfernt.
## 🧹 Abschluss
Die Desktop-App verfügt nun über eine saubere, wartbare Modulstruktur, die den Übergang von Prototyp-Code zu finalen
Feature-Komponenten unterstützt.

View File

@ -70,7 +70,7 @@ fun StammdatenTabContent(
// ohne die Abhängigkeit zu haben. In einer echten Architektur kommt dies über das Repository. // ohne die Abhängigkeit zu haben. In einer echten Architektur kommt dies über das Repository.
// Aber für die Demo/Fakten-Präsentation im Desktop-Shell-Kontext: // Aber für die Demo/Fakten-Präsentation im Desktop-Shell-Kontext:
try { try {
val clazz = Class.forName("at.mocode.desktop.v2.TurnierStoreV2") val clazz = Class.forName("at.mocode.desktop.data.TurnierStore")
val method = clazz.getMethod("allTurniere") val method = clazz.getMethod("allTurniere")
val all = method.invoke(null) as? List<*> val all = method.invoke(null) as? List<*>
val turnier = all?.find { t -> val turnier = all?.find { t ->

View File

@ -1,4 +1,4 @@
package at.mocode.desktop.v2 package at.mocode.desktop.data
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList

View File

@ -0,0 +1,36 @@
package at.mocode.desktop.data
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
data class Turnier(
val id: Long,
val veranstaltungId: Long,
val turnierNr: Int,
var typ: String = "ÖTO (National)",
var znsDataLoaded: Boolean = false,
var sparten: SnapshotStateList<String> = mutableStateListOf(),
var klassen: SnapshotStateList<String> = mutableStateListOf(),
var kategorie: SnapshotStateList<String> = mutableStateListOf(),
var datumVon: String,
var datumBis: String?,
var titel: String = "",
var subTitel: String = "",
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
)
object TurnierStore {
private val map = mutableMapOf<Long, MutableList<Turnier>>()
fun list(veranstaltungId: Long): MutableList<Turnier> = map.getOrPut(veranstaltungId) { mutableListOf() }
fun add(veranstaltungId: Long, t: Turnier) {
list(veranstaltungId).add(0, t)
}
fun remove(veranstaltungId: Long, tId: Long) {
list(veranstaltungId).removeAll { it.id == tId }
}
// Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab)
@JvmStatic
fun allTurniere(): List<Turnier> = map.values.flatten()
}

View File

@ -48,7 +48,7 @@ fun main() = application {
} }
println("[DesktopApp] KOIN initialisiert") println("[DesktopApp] KOIN initialisiert")
// Testdaten für Prototyp laden // Testdaten für Prototyp laden
at.mocode.desktop.v2.Store.seed() at.mocode.desktop.data.Store.seed()
} catch (e: Exception) { } catch (e: Exception) {
println("[DesktopApp] Koin-Warnung: ${e.message}") println("[DesktopApp] Koin-Warnung: ${e.message}")
} }

View File

@ -16,8 +16,19 @@ import androidx.compose.ui.graphics.vector.ImageVector
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 at.mocode.desktop.data.Store
import at.mocode.desktop.data.Turnier
import at.mocode.desktop.data.TurnierStore
import at.mocode.desktop.screens.management.FunktionaerVerwaltungScreen
import at.mocode.desktop.screens.management.VeranstalterAuswahl
import at.mocode.desktop.screens.management.VeranstalterDetail
import at.mocode.desktop.screens.management.VeranstalterVerwaltungScreen
import at.mocode.desktop.screens.nennung.NennungsEingangScreen
import at.mocode.desktop.screens.onboarding.OnboardingScreen
import at.mocode.desktop.screens.onboarding.OnboardingSettings import at.mocode.desktop.screens.onboarding.OnboardingSettings
import at.mocode.desktop.screens.onboarding.SettingsManager import at.mocode.desktop.screens.onboarding.SettingsManager
import at.mocode.desktop.screens.profile.FunktionaerProfil
import at.mocode.desktop.screens.veranstaltung.*
import at.mocode.frontend.core.designsystem.theme.AppColors import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.frontend.core.domain.zns.ZnsImportProvider
@ -520,10 +531,10 @@ private fun DesktopContentArea(
when (currentScreen) { when (currentScreen) {
// Onboarding (Geräte-Setup) // Onboarding (Geräte-Setup)
is AppScreen.Onboarding -> { is AppScreen.Onboarding -> {
at.mocode.desktop.v2.OnboardingScreen( OnboardingScreen(
settings = settings, settings = settings,
onSettingsChange = onSettingsChange, onSettingsChange = onSettingsChange,
onContinue = { finalSettings -> onContinue = { finalSettings: OnboardingSettings ->
SettingsManager.saveSettings(finalSettings) SettingsManager.saveSettings(finalSettings)
onNavigate(AppScreen.VeranstaltungVerwaltung) onNavigate(AppScreen.VeranstaltungVerwaltung)
} }
@ -532,8 +543,8 @@ private fun DesktopContentArea(
// Haupt-Zentrale: Veranstaltung-Verwaltung // Haupt-Zentrale: Veranstaltung-Verwaltung
is AppScreen.VeranstaltungVerwaltung -> { is AppScreen.VeranstaltungVerwaltung -> {
at.mocode.desktop.v2.VeranstaltungVerwaltung( VeranstaltungVerwaltung(
onVeranstaltungOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }, onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) },
onNewVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig()) }, onNewVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig()) },
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) }, onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) }, onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
@ -598,27 +609,27 @@ private fun DesktopContentArea(
} }
// --- Funktionaer-Verwaltung & Profil --- // --- Funktionaer-Verwaltung & Profil ---
is AppScreen.FunktionaerVerwaltung -> at.mocode.desktop.v2.FunktionaerVerwaltungScreen( is AppScreen.FunktionaerVerwaltung -> FunktionaerVerwaltungScreen(
onBack = onBack, onBack = onBack,
onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) } onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) }
) )
is AppScreen.FunktionaerProfil -> at.mocode.desktop.v2.FunktionaerProfilV2( is AppScreen.FunktionaerProfil -> FunktionaerProfil(
id = currentScreen.id, id = currentScreen.id,
onBack = onBack, onBack = onBack,
) )
// --- Veranstalter-Verwaltung & Profil --- // --- Veranstalter-Verwaltung & Profil ---
is AppScreen.VeranstalterVerwaltung -> at.mocode.desktop.v2.VeranstalterVerwaltungScreen( is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
onBack = onBack, onBack = onBack,
onNew = { onNavigate(AppScreen.VeranstalterNeu) }, onNew = { onNavigate(AppScreen.VeranstalterNeu) },
onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) } onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) }
) )
is AppScreen.VeranstalterProfil -> at.mocode.desktop.v2.VeranstalterDetailV2( is AppScreen.VeranstalterProfil -> VeranstalterDetail(
veranstalterId = currentScreen.id, veranstalterId = currentScreen.id,
onBack = onBack, onBack = onBack,
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) }, onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(currentScreen.id)) }, onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(currentScreen.id)) },
) )
@ -629,38 +640,31 @@ private fun DesktopContentArea(
*/ */
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht // Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2( is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
onBack = onBack, onBack = onBack,
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) }, onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
) )
is AppScreen.VeranstalterNeu -> at.mocode.desktop.v2.VeranstalterAnlegenWizard( is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard(
onCancel = onBack, onCancel = onBack,
onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) } onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
) )
is AppScreen.VeranstalterDetail -> { is AppScreen.VeranstalterDetail -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId
if (vId != 1L) { // Temporärer Check für Mock-Daten VeranstalterDetail(
InvalidContextNotice( veranstalterId = vId,
message = "Veranstalter (ID=$vId) nicht gefunden.", onBack = onBack,
onBack = onBack onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
) onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
} else { )
at.mocode.desktop.v2.VeranstalterDetailV2(
veranstalterId = vId,
onBack = onBack,
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
)
}
} }
is AppScreen.VeranstaltungKonfig -> { is AppScreen.VeranstaltungKonfig -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId
// Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard // Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard
at.mocode.desktop.v2.VeranstaltungKonfig( VeranstaltungKonfig(
veranstalterId = vId, veranstalterId = vId,
onBack = onBack, onBack = onBack,
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) }, onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) },
@ -671,33 +675,33 @@ private fun DesktopContentArea(
is AppScreen.VeranstaltungProfil -> { is AppScreen.VeranstaltungProfil -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId
val evtId = currentScreen.veranstaltungId val evtId = currentScreen.veranstaltungId
if (at.mocode.desktop.v2.Store.vereine.none { it.id == vId }) { if (Store.vereine.none { it.id == vId }) {
InvalidContextNotice( InvalidContextNotice(
message = "Veranstalter (ID=$vId) nicht gefunden.", message = "Veranstalter (ID=$vId) nicht gefunden.",
onBack = onBack onBack = onBack
) )
} else if (at.mocode.desktop.v2.Store.eventsFor(vId).none { it.id == evtId }) { } else if (Store.eventsFor(vId).none { it.id == evtId }) {
InvalidContextNotice( InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.", message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
onBack = onBack onBack = onBack
) )
} else { } else {
at.mocode.desktop.v2.VeranstaltungProfilScreen( VeranstaltungProfilScreen(
veranstalterId = vId, veranstalterId = vId,
veranstaltungId = evtId, veranstaltungId = evtId,
onBack = onBack, onBack = onBack,
onTurnierNeu = { onTurnierNeu = {
val veranstaltung = at.mocode.desktop.v2.Store.eventsFor(vId).firstOrNull { it.id == evtId } val veranstaltung = Store.eventsFor(vId).firstOrNull { it.id == evtId }
val list = at.mocode.desktop.v2.TurnierStore.list(evtId) val list = TurnierStore.list(evtId)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.Turnier( val draft = Turnier(
id = newId, id = newId,
veranstaltungId = evtId, veranstaltungId = evtId,
turnierNr = 0, turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "", datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis, datumBis = veranstaltung?.datumBis,
) )
at.mocode.desktop.v2.TurnierStore.add(evtId, draft) TurnierStore.add(evtId, draft)
onNavigate(AppScreen.TurnierDetail(evtId, newId)) onNavigate(AppScreen.TurnierDetail(evtId, newId))
}, },
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) }, onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
@ -711,21 +715,21 @@ private fun DesktopContentArea(
veranstaltungId = currentScreen.id, veranstaltungId = currentScreen.id,
onBack = onBack, onBack = onBack,
onTurnierNeu = { onTurnierNeu = {
val v = at.mocode.desktop.v2.Store.vereine.firstOrNull { vv -> val v = Store.vereine.firstOrNull { vv ->
at.mocode.desktop.v2.Store.eventsFor(vv.id).any { it.id == currentScreen.id } Store.eventsFor(vv.id).any { it.id == currentScreen.id }
} }
val veranstaltung = val veranstaltung =
v?.let { at.mocode.desktop.v2.Store.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } } v?.let { Store.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } }
val list = at.mocode.desktop.v2.TurnierStore.list(currentScreen.id) val list = TurnierStore.list(currentScreen.id)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.Turnier( val draft = Turnier(
id = newId, id = newId,
veranstaltungId = currentScreen.id, veranstaltungId = currentScreen.id,
turnierNr = 0, turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "", datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis, datumBis = veranstaltung?.datumBis,
) )
at.mocode.desktop.v2.TurnierStore.add(currentScreen.id, draft) TurnierStore.add(currentScreen.id, draft)
onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId)) onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId))
}, },
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) }, onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
@ -739,8 +743,8 @@ private fun DesktopContentArea(
// Turnier-Screens // Turnier-Screens
is AppScreen.TurnierDetail -> { is AppScreen.TurnierDetail -> {
val evtId = currentScreen.veranstaltungId val evtId = currentScreen.veranstaltungId
val parent = at.mocode.desktop.v2.Store.vereine.firstOrNull { v -> val parent = Store.vereine.firstOrNull { v ->
at.mocode.desktop.v2.Store.eventsFor(v.id).any { it.id == evtId } Store.eventsFor(v.id).any { it.id == evtId }
} }
if (parent == null) { if (parent == null) {
InvalidContextNotice( InvalidContextNotice(
@ -748,7 +752,7 @@ private fun DesktopContentArea(
onBack = onBack onBack = onBack
) )
} else { } else {
val veranstaltung = at.mocode.desktop.v2.Store.eventsFor(parent.id).firstOrNull { it.id == evtId } val veranstaltung = Store.eventsFor(parent.id).firstOrNull { it.id == evtId }
val blCode = parent.oepsNummer.split("-").getOrNull(1) ?: "" val blCode = parent.oepsNummer.split("-").getOrNull(1) ?: ""
val bundesland = mapOepsToBundesland(blCode) val bundesland = mapOepsToBundesland(blCode)
TurnierDetailScreen( TurnierDetailScreen(
@ -768,9 +772,8 @@ private fun DesktopContentArea(
is AppScreen.TurnierNeu -> { is AppScreen.TurnierNeu -> {
val evtId = currentScreen.veranstaltungId val evtId = currentScreen.veranstaltungId
// V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert val parent = Store.vereine.firstOrNull { v ->
val parent = at.mocode.desktop.v2.Store.vereine.firstOrNull { v -> Store.eventsFor(v.id).any { it.id == evtId }
at.mocode.desktop.v2.Store.eventsFor(v.id).any { it.id == evtId }
} }
if (parent == null) { if (parent == null) {
InvalidContextNotice( InvalidContextNotice(
@ -778,7 +781,7 @@ private fun DesktopContentArea(
onBack = onBack onBack = onBack
) )
} else { } else {
at.mocode.desktop.v2.TurnierWizard( TurnierWizard(
veranstalterId = parent.id, veranstalterId = parent.id,
veranstaltungId = evtId, veranstaltungId = evtId,
onBack = onBack, onBack = onBack,
@ -839,7 +842,7 @@ private fun DesktopContentArea(
} }
is AppScreen.NennungsEingang -> { is AppScreen.NennungsEingang -> {
at.mocode.desktop.v2.NennungsEingangScreen( NennungsEingangScreen(
onBack = onBack onBack = onBack
) )
} }

View File

@ -1,4 +1,4 @@
package at.mocode.desktop.v2 package at.mocode.desktop.screens.management
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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 at.mocode.desktop.data.Store
@Composable @Composable
fun <T> ManagementTableScreen( fun <T> ManagementTableScreen(
@ -253,8 +254,8 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
@Composable @Composable
fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) { fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) {
// Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten,
// Wir nutzen hier die 'vereine' Liste aus dem Store. // wir nutzen hier die 'vereine' Liste aus dem Store.
val vereine = Store.vereine val vereine = Store.vereine
var filter by remember { mutableStateOf("") } var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { val filteredItems = if (filter.isEmpty()) vereine else vereine.filter {

View File

@ -0,0 +1,207 @@
package at.mocode.desktop.screens.management
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.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ChevronRight
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.unit.dp
import at.mocode.desktop.data.Store
import at.mocode.desktop.theme.DesktopTheme
@Composable
fun VeranstalterAuswahl(
onBack: () -> Unit,
onWeiter: (Long) -> Unit,
onNeu: () -> Unit,
) {
DesktopTheme {
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück",
modifier = Modifier.clickable { onBack() })
Text("Veranstalter auswählen", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.weight(1f))
OutlinedButton(onClick = onNeu) { Text("+ Neuer Veranstalter") }
}
var selectedId by remember { mutableStateOf<Long?>(null) }
LazyColumn(Modifier.fillMaxSize().weight(1f)) {
items(Store.vereine) { v ->
val sel = selectedId == v.id
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clickable { selectedId = v.id },
colors = if (sel) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
else CardDefaults.cardColors()
) {
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(40.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
Text((v.kurzname ?: v.name).take(2).uppercase(), color = Color.White)
}
Spacer(Modifier.width(12.dp))
Column {
Text(v.name, style = MaterialTheme.typography.titleMedium)
Text("OEPS: ${v.oepsNummer} · ${v.ort ?: ""}", style = MaterialTheme.typography.bodySmall)
}
}
}
}
}
Button(
onClick = { selectedId?.let(onWeiter) },
enabled = selectedId != null,
modifier = Modifier.fillMaxWidth()
) { Text("Weiter zum Veranstalter") }
}
}
}
@Composable
fun VeranstalterDetail(
veranstalterId: Long,
onBack: () -> Unit,
onZurVeranstaltung: (Long) -> Unit,
onNeuVeranstaltung: () -> Unit,
) {
DesktopTheme {
val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } }
if (verein == null) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Veranstalter nicht gefunden")
}
return@DesktopTheme
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück",
modifier = Modifier.clickable { onBack() })
Text(verein.name, style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.weight(1f))
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
Text(
(verein.kurzname ?: verein.name).take(2).uppercase(),
color = Color.White,
fontWeight = FontWeight.SemiBold
)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(verein.name, style = MaterialTheme.typography.titleMedium)
val line2 = listOfNotNull(
"OEPS: ${verein.oepsNummer}",
verein.ort,
verein.plz,
verein.strasse
).filter { it.isNotBlank() }.joinToString(" · ")
if (line2.isNotBlank()) Text(line2, color = Color(0xFF6B7280))
val line3 = listOfNotNull(verein.email, verein.telefon).filter { it.isNotBlank() }.joinToString(" · ")
if (line3.isNotBlank()) Text(line3, color = Color(0xFF6B7280))
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var name by remember { mutableStateOf(verein.name) }
var oeps by remember { mutableStateOf(verein.oepsNummer) }
var ort by remember { mutableStateOf(verein.ort ?: "") }
var plz by remember { mutableStateOf(verein.plz ?: "") }
var strasse by remember { mutableStateOf(verein.strasse ?: "") }
var email by remember { mutableStateOf(verein.email ?: "") }
var tel by remember { mutableStateOf(verein.telefon ?: "") }
var logo by remember { mutableStateOf(verein.logoUrl ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
verein.name = name
verein.oepsNummer = oeps
verein.ort = ort.ifBlank { null }
verein.plz = plz.ifBlank { null }
verein.strasse = strasse.ifBlank { null }
verein.email = email.ifBlank { null }
verein.telefon = tel.ifBlank { null }
verein.logoUrl = logo.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Veranstalter bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(oeps, { oeps = it }, label = { Text("OEPS-Nummer") }, modifier = Modifier.weight(1f))
OutlinedTextField(logo, { logo = it }, label = { Text("Logo-URL") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(ort, { ort = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
OutlinedTextField(plz, { plz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
}
OutlinedTextField(
strasse,
{ strasse = it },
label = { Text("Straße") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.weight(1f))
OutlinedTextField(tel, { tel = it }, label = { Text("Telefon") }, modifier = Modifier.weight(1f))
}
}
}
)
}
Text("Veranstaltungen", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
val events = remember(veranstalterId) { Store.eventsFor(veranstalterId) }
if (events.isEmpty()) {
Text("Keine Veranstaltungen angelegt", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
} else {
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(events) { ev ->
ListItem(
headlineContent = { Text(ev.titel) },
supportingContent = { Text("${ev.datumVon} bis ${ev.datumBis ?: "?"} · ${ev.status}") },
trailingContent = { Icon(Icons.Default.ChevronRight, null) },
modifier = Modifier.clickable { onZurVeranstaltung(ev.id) }
)
}
}
}
}
}
}

View File

@ -0,0 +1,300 @@
package at.mocode.desktop.screens.nennung
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.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
import androidx.compose.ui.Modifier
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 at.mocode.desktop.theme.DesktopTheme
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
import at.mocode.frontend.features.nennung.domain.NennungResponse
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
data class OnlineNennungMail(
val id: String,
val sender: String,
val empfaenger: String,
val datum: String,
val turnierNr: String,
val vorname: String,
val nachname: String,
val lizenz: String,
val pferd: String,
val pferdAlter: String,
val telefon: String?,
val bewerbe: String,
val bemerkungen: String?,
var status: String = "NEU"
)
fun NennungResponse.toMail() = OnlineNennungMail(
id = id,
sender = email,
empfaenger = "Meldestelle",
datum = "-", // Datum ist in Entity nicht direkt drin, könnte man ergänzen
turnierNr = turnierNr,
vorname = vorname,
nachname = nachname,
lizenz = lizenz,
pferd = pferdName,
pferdAlter = pferdAlter,
telefon = telefon,
bewerbe = bewerbe,
bemerkungen = bemerkungen,
status = if (status == "GELESEN") "GELESEN" else "NEU"
)
@Composable
fun NennungsEingangScreen(onBack: () -> Unit) {
val repository: NennungRemoteRepository = koinInject()
val scope = rememberCoroutineScope()
DesktopTheme {
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 refresh = {
scope.launch {
isRefreshing = true
repository.holeNennungen().onSuccess { response ->
mails = response.map { it.toMail() }
}.onFailure {
// Fallback oder Fehleranzeige
if (mails.isEmpty()) mails = getMockMails()
}
isRefreshing = 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) {
refresh()
}
if (selectedMail != null) {
NennungDetailDialog(
mail = selectedMail!!,
onDismiss = { selectedMail = null },
onMarkProcessed = {
scope.launch {
repository.markiereAlsGelesen(selectedMail!!.id)
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)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) }
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() },
enabled = !isRefreshing
) {
Icon(Icons.Default.Refresh, null)
Spacer(Modifier.width(8.dp))
Text("Aktualisieren")
}
}
Text(
"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 {
// Header Zeile
Row(
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(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(120.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
}
HorizontalDivider()
if (filteredMails.isEmpty() && !isRefreshing) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
if (searchQuery.isBlank()) "Keine neuen Nennungen vorhanden."
else "Keine Nennungen für '$searchQuery' gefunden.",
color = Color.Gray
)
}
} else {
LazyColumn(Modifier.fillMaxSize()) {
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(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)
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)
}
}
}
}
}
}
}
}
@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",
"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"
)
)

View File

@ -0,0 +1,144 @@
package at.mocode.desktop.screens.onboarding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.desktop.theme.DesktopTheme
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OnboardingScreen(
settings: OnboardingSettings,
onSettingsChange: (OnboardingSettings) -> Unit,
onContinue: (OnboardingSettings) -> Unit,
) {
var currentStep by remember { mutableStateOf(0) }
val discoveryService: NetworkDiscoveryService = koinInject()
val discoveredServices by remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
// Automatische Discovery starten, wenn wir auf Schritt 0 sind
LaunchedEffect(currentStep) {
if (currentStep == 0) discoveryService.startDiscovery()
}
DesktopTheme {
Surface(color = MaterialTheme.colorScheme.background) {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp).verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"Willkommen beim Meldestelle-Biest",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
Text(
if (currentStep == 0) "Schritt 1: Netzwerk-Rolle festlegen" else "Schritt 2: Rollenspezifische Konfiguration",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (currentStep == 0) {
// PHASE 1: NETZWERK-ROLLE
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("🌐 Netzwerk-Rolle wählen", style = MaterialTheme.typography.titleMedium)
Text(
"Wähle aus, ob dieses Gerät als Master (zentrale Datenbank) oder als Client fungiert.",
style = MaterialTheme.typography.bodySmall
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) },
shape = MaterialTheme.shapes.medium,
color = if (settings.networkRole == NetworkRole.MASTER) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth()
) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = settings.networkRole == NetworkRole.MASTER,
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) }
)
Column {
Text("Master (Host)", style = MaterialTheme.typography.labelLarge)
Text(
"Verwaltet die zentrale Datenbank und koordiniert den Sync.",
style = MaterialTheme.typography.bodySmall
)
}
}
}
Surface(
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) },
shape = MaterialTheme.shapes.medium,
color = if (settings.networkRole == NetworkRole.CLIENT) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth()
) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = settings.networkRole == NetworkRole.CLIENT,
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) }
)
Column {
Text("Client", style = MaterialTheme.typography.labelLarge)
Text(
"Verbindet sich mit einem Master-Gerät im lokalen Netzwerk.",
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
Button(
onClick = { currentStep = 1 },
modifier = Modifier.align(Alignment.End),
enabled = true
) {
Text("Weiter")
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
}
}
}
} else {
// PHASE 2: ROLLENSPEZIFISCH
Text("Konfiguration für ${settings.networkRole}")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = { currentStep = 0 }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
Spacer(Modifier.width(8.dp))
Text("Zurück zur Rollenauswahl")
}
Button(
onClick = { onContinue(settings) },
enabled = OnboardingValidator.canContinue(settings)
) {
Text("Konfiguration abschließen")
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
}
}
}
}
}
}
}

View File

@ -0,0 +1,326 @@
package at.mocode.desktop.screens.profile
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
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.unit.dp
import at.mocode.desktop.data.Store
import at.mocode.desktop.theme.DesktopTheme
import at.mocode.frontend.core.designsystem.components.MsTextField
@Composable
fun PferdProfil(id: Long, onBack: () -> Unit) {
DesktopTheme {
val pferd = remember(id) { Store.pferde.firstOrNull { it.id == id } }
if (pferd == null) {
Text("Pferd nicht gefunden"); return@DesktopTheme
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Pferde-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(56.dp).background(Color(0xFF374151), shape = MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
Text(pferd.name.take(2).uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(pferd.name, style = MaterialTheme.typography.titleMedium)
val l2 =
listOfNotNull(pferd.oepsNummer?.let { "OEPS: $it" }, pferd.feiId?.let { "FEI: $it" }).joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
val l3 = listOfNotNull(pferd.geburtsdatum?.let { "geb. $it" }, pferd.farbe).joinToString(" · ")
if (l3.isNotBlank()) Text(l3, color = Color(0xFF6B7280))
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var name by remember { mutableStateOf(pferd.name) }
var oeps by remember { mutableStateOf(pferd.oepsNummer ?: "") }
var fei by remember { mutableStateOf(pferd.feiId ?: "") }
var geb by remember { mutableStateOf(pferd.geburtsdatum ?: "") }
var farbe by remember { mutableStateOf(pferd.farbe ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
pferd.name = name
pferd.oepsNummer = oeps.ifBlank { null }
pferd.feiId = fei.ifBlank { null }
pferd.geburtsdatum = geb.ifBlank { null }
pferd.farbe = farbe.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Pferd bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(name, { name = it }, label = "Name", modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(geb, { geb = it }, label = "Geburtsdatum", modifier = Modifier.weight(1f))
MsTextField(farbe, { farbe = it }, label = "Farbe", modifier = Modifier.weight(1f))
}
}
}
)
}
}
}
}
@Composable
fun ReiterProfil(id: Long, onBack: () -> Unit) {
DesktopTheme {
val r = remember(id) { Store.reiter.firstOrNull { it.id == id } }
if (r == null) {
Text("Reiter nicht gefunden"); return@DesktopTheme
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Reiter-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(56.dp).background(Color(0xFF1E3A8A), shape = MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
Text(
(r.vorname.take(1) + r.nachname.take(1)).uppercase(),
color = Color.White,
fontWeight = FontWeight.SemiBold
)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("${r.vorname} ${r.nachname}", style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull(r.oepsNummer?.let { "OEPS: $it" }, r.nation).joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
val l3 = listOfNotNull("Lizenz: ${r.lizenzKlasse}", r.verein).joinToString(" · ")
if (l3.isNotBlank()) Text(l3, color = Color(0xFF6B7280))
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var vor by remember { mutableStateOf(r.vorname) }
var nach by remember { mutableStateOf(r.nachname) }
var oeps by remember { mutableStateOf(r.oepsNummer ?: "") }
var liz by remember { mutableStateOf(r.lizenzKlasse) }
var verein by remember { mutableStateOf(r.verein ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
r.vorname = vor
r.nachname = nach
r.oepsNummer = oeps.ifBlank { null }
r.lizenzKlasse = liz
r.verein = verein.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Reiter bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(vor, { vor = it }, label = "Vorname", modifier = Modifier.weight(1f))
MsTextField(nach, { nach = it }, label = "Nachname", modifier = Modifier.weight(1f))
}
MsTextField(oeps, { oeps = it }, label = "OEPS-Nr.", modifier = Modifier.fillMaxWidth())
MsTextField(liz, { liz = it }, label = "Lizenzklasse", modifier = Modifier.fillMaxWidth())
MsTextField(verein, { verein = it }, label = "Verein", modifier = Modifier.fillMaxWidth())
}
}
)
}
}
}
}
@Composable
fun VereinProfil(id: Long, onBack: () -> Unit) {
DesktopTheme {
val v = remember(id) { Store.vereine.firstOrNull { it.id == id } }
if (v == null) {
Text("Verein nicht gefunden"); return@DesktopTheme
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Vereins-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
Text((v.kurzname ?: v.name).take(2).uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(v.name, style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull("OEPS: ${v.oepsNummer}", v.ort, v.plz, v.strasse).filter { it.isNotBlank() }
.joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
val l3 = listOfNotNull(v.email, v.telefon).filter { it.isNotBlank() }.joinToString(" · ")
if (l3.isNotBlank()) Text(l3, color = Color(0xFF6B7280))
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var name by remember { mutableStateOf(v.name) }
var oeps by remember { mutableStateOf(v.oepsNummer) }
var ort by remember { mutableStateOf(v.ort ?: "") }
var plz by remember { mutableStateOf(v.plz ?: "") }
var strasse by remember { mutableStateOf(v.strasse ?: "") }
var email by remember { mutableStateOf(v.email ?: "") }
var tel by remember { mutableStateOf(v.telefon ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
v.name = name
v.oepsNummer = oeps
v.ort = ort.ifBlank { null }
v.plz = plz.ifBlank { null }
v.strasse = strasse.ifBlank { null }
v.email = email.ifBlank { null }
v.telefon = tel.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Verein bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(name, { name = it }, label = "Name", modifier = Modifier.fillMaxWidth())
MsTextField(oeps, { oeps = it }, label = "OEPS-Nr.", modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(ort, { ort = it }, label = "Ort", modifier = Modifier.weight(1f))
MsTextField(plz, { plz = it }, label = "PLZ", modifier = Modifier.weight(1f))
}
MsTextField(strasse, { strasse = it }, label = "Straße", modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(email, { email = it }, label = "E-Mail", modifier = Modifier.weight(1f))
MsTextField(tel, { tel = it }, label = "Telefon", modifier = Modifier.weight(1f))
}
}
}
)
}
}
}
}
@Composable
fun FunktionaerProfil(id: Long, onBack: () -> Unit) {
DesktopTheme {
val f = remember(id) { Store.funktionaere.firstOrNull { it.id == id } }
if (f == null) {
Text("Funktionär nicht gefunden"); return@DesktopTheme
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Funktionärs-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(56.dp).background(Color(0xFF111827), shape = MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
val initials =
(f.vorname + " " + f.nachname).trim().split(" ").mapNotNull { it.firstOrNull()?.toString() }.take(2)
.joinToString("")
Text(initials.uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("${f.vorname} ${f.nachname}", style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull(
f.richterNummer?.let { "Nr. $it" },
f.richterQualifikation?.let { "Qual.: $it" }).joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
f.email?.let { Text(it, color = Color(0xFF6B7280)) }
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var vor by remember { mutableStateOf(f.vorname) }
var nach by remember { mutableStateOf(f.nachname) }
var num by remember { mutableStateOf(f.richterNummer ?: "") }
var qual by remember { mutableStateOf(f.richterQualifikation ?: "") }
var email by remember { mutableStateOf(f.email ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
f.vorname = vor
f.nachname = nach
f.richterNummer = num.ifBlank { null }
f.richterQualifikation = qual.ifBlank { null }
f.email = email.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Funktionär bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(vor, { vor = it }, label = "Vorname", modifier = Modifier.weight(1f))
MsTextField(nach, { nach = it }, label = "Nachname", modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MsTextField(num, { num = it }, label = "Nummer", modifier = Modifier.weight(1f))
MsTextField(qual, { qual = it }, label = "Qualifikation", modifier = Modifier.weight(1f))
}
MsTextField(email, { email = it }, label = "E-Mail", modifier = Modifier.fillMaxWidth())
}
}
)
}
}
}
}

View File

@ -1,4 +1,4 @@
package at.mocode.desktop.v2 package at.mocode.desktop.screens.veranstaltung
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -21,6 +21,11 @@ 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 at.mocode.desktop.data.Store
import at.mocode.desktop.data.Turnier
import at.mocode.desktop.data.TurnierStore
import at.mocode.desktop.data.Veranstaltung
import at.mocode.desktop.theme.DesktopTheme
import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import org.koin.compose.koinInject import org.koin.compose.koinInject
import java.time.Instant import java.time.Instant
@ -43,7 +48,7 @@ fun VeranstaltungVerwaltung(
onNavigateToVeranstalter: () -> Unit, onNavigateToVeranstalter: () -> Unit,
onNavigateToZnsImport: () -> Unit onNavigateToZnsImport: () -> Unit
) { ) {
DesktopThemeV2 { DesktopTheme {
val allVeranstaltungen = remember { Store.allEvents() } val allVeranstaltungen = remember { Store.allEvents() }
val vereine = Store.vereine val vereine = Store.vereine
@ -823,7 +828,7 @@ fun VeranstaltungKonfig(
val znsImporter: ZnsImportProvider = koinInject() val znsImporter: ZnsImportProvider = koinInject()
val znsState = znsImporter.state val znsState = znsImporter.state
DesktopThemeV2 { DesktopTheme {
var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) } var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) }
var selectedVereinId by remember { mutableStateOf(veranstalterId) } var selectedVereinId by remember { mutableStateOf(veranstalterId) }
var titel by remember { mutableStateOf("") } var titel by remember { mutableStateOf("") }
@ -968,34 +973,6 @@ fun VeranstaltungKonfig(
} }
} }
data class Turnier(
val id: Long,
val veranstaltungId: Long,
val turnierNr: Int,
var typ: String = "ÖTO (National)",
var znsDataLoaded: Boolean = false,
var sparten: SnapshotStateList<String> = mutableStateListOf(),
var klassen: SnapshotStateList<String> = mutableStateListOf(),
var kategorie: SnapshotStateList<String> = mutableStateListOf(),
var datumVon: String,
var datumBis: String?,
var titel: String = "",
var subTitel: String = "",
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
)
object TurnierStore {
private val map = mutableMapOf<Long, MutableList<Turnier>>()
fun list(veranstaltungId: Long): MutableList<Turnier> = map.getOrPut(veranstaltungId) { mutableListOf() }
fun add(veranstaltungId: Long, t: Turnier) {
list(veranstaltungId).add(0, t)
}
fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } }
// Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab)
@JvmStatic
fun allTurniere(): List<Turnier> = map.values.flatten()
}
@Composable @Composable
fun VeranstaltungProfilScreen( fun VeranstaltungProfilScreen(
@ -1006,7 +983,7 @@ fun VeranstaltungProfilScreen(
onTurnierOpen: (Long) -> Unit, onTurnierOpen: (Long) -> Unit,
onNavigateToVeranstalterProfil: (Long) -> Unit, onNavigateToVeranstalterProfil: (Long) -> Unit,
) { ) {
DesktopThemeV2 { DesktopTheme {
val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) } val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) }
@ -1228,7 +1205,7 @@ fun TurnierWizard(
onBack: () -> Unit, onBack: () -> Unit,
onSaved: (Long) -> Unit, onSaved: (Long) -> Unit,
) { ) {
DesktopThemeV2 { DesktopTheme {
val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
var currentStep by remember { mutableStateOf(1) } var currentStep by remember { mutableStateOf(1) }
var showZnsDialog by remember { mutableStateOf(false) } var showZnsDialog by remember { mutableStateOf(false) }

View File

@ -1,9 +1,8 @@
package at.mocode.desktop.v2 package at.mocode.desktop.theme
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -26,7 +25,7 @@ private val LightColors: ColorScheme = lightColorScheme(
) )
@Composable @Composable
fun DesktopThemeV2(content: @Composable () -> Unit) { fun DesktopTheme(content: @Composable () -> Unit) {
MaterialTheme( MaterialTheme(
colorScheme = LightColors, colorScheme = LightColors,
typography = Typography(), typography = Typography(),

View File

@ -1,257 +0,0 @@
package at.mocode.desktop.v2
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.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
import androidx.compose.ui.Modifier
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 at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
import at.mocode.frontend.features.nennung.domain.NennungResponse
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
data class OnlineNennungMail(
val id: String,
val sender: String,
val empfaenger: String,
val datum: String,
val turnierNr: String,
val vorname: String,
val nachname: String,
val lizenz: String,
val pferd: String,
val pferdAlter: String,
val telefon: String?,
val bewerbe: String,
val bemerkungen: String?,
var status: String = "NEU"
)
fun NennungResponse.toMail() = OnlineNennungMail(
id = id,
sender = email,
empfaenger = "Meldestelle",
datum = "-", // Datum ist in Entity nicht direkt drin, könnte man ergänzen
turnierNr = turnierNr,
vorname = vorname,
nachname = nachname,
lizenz = lizenz,
pferd = pferdName,
pferdAlter = pferdAlter,
telefon = telefon,
bewerbe = bewerbe,
bemerkungen = bemerkungen,
status = if (status == "GELESEN") "GELESEN" else "NEU"
)
@Composable
fun NennungsEingangScreen(onBack: () -> Unit) {
val repository: NennungRemoteRepository = koinInject()
val scope = rememberCoroutineScope()
DesktopThemeV2 {
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 refresh = {
scope.launch {
isRefreshing = true
repository.holeNennungen().onSuccess { response ->
mails = response.map { it.toMail() }
}.onFailure {
// Fallback oder Fehleranzeige
if (mails.isEmpty()) mails = getMockMails()
}
isRefreshing = 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) {
refresh()
}
if (selectedMail != null) {
NennungDetailDialog(
mail = selectedMail!!,
onDismiss = { selectedMail = null },
onMarkProcessed = {
scope.launch {
repository.markiereAlsGelesen(selectedMail!!.id)
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)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) }
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() },
enabled = !isRefreshing
) {
Icon(Icons.Default.Refresh, null)
Spacer(Modifier.width(8.dp))
Text("Aktualisieren")
}
}
Text(
"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 {
// Header Zeile
Row(
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(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(120.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
}
HorizontalDivider()
if (filteredMails.isEmpty() && !isRefreshing) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
if (searchQuery.isBlank()) "Keine neuen Nennungen vorhanden."
else "Keine Nennungen für '$searchQuery' gefunden.",
color = Color.Gray
)
}
} else {
LazyColumn(Modifier.fillMaxSize()) {
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(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)
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)
}
}
}
}
}
}
}
}
@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", "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")
)