diff --git a/docs/99_Journal/2026-04-17_Desktop-Reorganisation-V2-Removal.md b/docs/99_Journal/2026-04-17_Desktop-Reorganisation-V2-Removal.md new file mode 100644 index 00000000..8961e07c --- /dev/null +++ b/docs/99_Journal/2026-04-17_Desktop-Reorganisation-V2-Removal.md @@ -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. diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierStammdatenTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierStammdatenTab.kt index fe119c43..afd771b4 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierStammdatenTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierStammdatenTab.kt @@ -70,7 +70,7 @@ fun StammdatenTabContent( // 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: 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 all = method.invoke(null) as? List<*> val turnier = all?.find { t -> diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Stores.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/data/Stores.kt similarity index 99% rename from frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Stores.kt rename to frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/data/Stores.kt index 336b0dfe..929065e4 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Stores.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/data/Stores.kt @@ -1,4 +1,4 @@ -package at.mocode.desktop.v2 +package at.mocode.desktop.data import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/data/TurnierStore.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/data/TurnierStore.kt new file mode 100644 index 00000000..70e1513c --- /dev/null +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/data/TurnierStore.kt @@ -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 = mutableStateListOf(), + var klassen: SnapshotStateList = mutableStateListOf(), + var kategorie: SnapshotStateList = mutableStateListOf(), + var datumVon: String, + var datumBis: String?, + var titel: String = "", + var subTitel: String = "", + var sponsoren: SnapshotStateList = mutableStateListOf(), +) + +object TurnierStore { + private val map = mutableMapOf>() + fun list(veranstaltungId: Long): MutableList = 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 = map.values.flatten() +} diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt index ca578ebc..d9d01e49 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt @@ -48,7 +48,7 @@ fun main() = application { } println("[DesktopApp] KOIN initialisiert") // Testdaten für Prototyp laden - at.mocode.desktop.v2.Store.seed() + at.mocode.desktop.data.Store.seed() } catch (e: Exception) { println("[DesktopApp] Koin-Warnung: ${e.message}") } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt index d9bc8b2d..716e03d3 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt @@ -16,8 +16,19 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp 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.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.Dimens import at.mocode.frontend.core.domain.zns.ZnsImportProvider @@ -520,10 +531,10 @@ private fun DesktopContentArea( when (currentScreen) { // Onboarding (Geräte-Setup) is AppScreen.Onboarding -> { - at.mocode.desktop.v2.OnboardingScreen( + OnboardingScreen( settings = settings, onSettingsChange = onSettingsChange, - onContinue = { finalSettings -> + onContinue = { finalSettings: OnboardingSettings -> SettingsManager.saveSettings(finalSettings) onNavigate(AppScreen.VeranstaltungVerwaltung) } @@ -532,8 +543,8 @@ private fun DesktopContentArea( // Haupt-Zentrale: Veranstaltung-Verwaltung is AppScreen.VeranstaltungVerwaltung -> { - at.mocode.desktop.v2.VeranstaltungVerwaltung( - onVeranstaltungOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }, + VeranstaltungVerwaltung( + onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }, onNewVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig()) }, onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) }, onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) }, @@ -598,27 +609,27 @@ private fun DesktopContentArea( } // --- Funktionaer-Verwaltung & Profil --- - is AppScreen.FunktionaerVerwaltung -> at.mocode.desktop.v2.FunktionaerVerwaltungScreen( + is AppScreen.FunktionaerVerwaltung -> FunktionaerVerwaltungScreen( onBack = onBack, onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) } ) - is AppScreen.FunktionaerProfil -> at.mocode.desktop.v2.FunktionaerProfilV2( + is AppScreen.FunktionaerProfil -> FunktionaerProfil( id = currentScreen.id, onBack = onBack, ) // --- Veranstalter-Verwaltung & Profil --- - is AppScreen.VeranstalterVerwaltung -> at.mocode.desktop.v2.VeranstalterVerwaltungScreen( + is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen( onBack = onBack, onNew = { onNavigate(AppScreen.VeranstalterNeu) }, onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) } ) - is AppScreen.VeranstalterProfil -> at.mocode.desktop.v2.VeranstalterDetailV2( + is AppScreen.VeranstalterProfil -> VeranstalterDetail( veranstalterId = currentScreen.id, 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)) }, ) @@ -629,38 +640,31 @@ private fun DesktopContentArea( */ // Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht - is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2( + is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl( onBack = onBack, onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, ) - is AppScreen.VeranstalterNeu -> at.mocode.desktop.v2.VeranstalterAnlegenWizard( + is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard( onCancel = onBack, onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) } ) is AppScreen.VeranstalterDetail -> { val vId = currentScreen.veranstalterId - if (vId != 1L) { // Temporärer Check für Mock-Daten - InvalidContextNotice( - message = "Veranstalter (ID=$vId) nicht gefunden.", - onBack = onBack - ) - } else { - at.mocode.desktop.v2.VeranstalterDetailV2( - veranstalterId = vId, - onBack = onBack, - onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) }, - onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) }, - ) - } + VeranstalterDetail( + veranstalterId = vId, + onBack = onBack, + onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) }, + onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) }, + ) } is AppScreen.VeranstaltungKonfig -> { val vId = currentScreen.veranstalterId // Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard - at.mocode.desktop.v2.VeranstaltungKonfig( + VeranstaltungKonfig( veranstalterId = vId, onBack = onBack, onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) }, @@ -671,33 +675,33 @@ private fun DesktopContentArea( is AppScreen.VeranstaltungProfil -> { val vId = currentScreen.veranstalterId val evtId = currentScreen.veranstaltungId - if (at.mocode.desktop.v2.Store.vereine.none { it.id == vId }) { + if (Store.vereine.none { it.id == vId }) { InvalidContextNotice( message = "Veranstalter (ID=$vId) nicht gefunden.", 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( message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.", onBack = onBack ) } else { - at.mocode.desktop.v2.VeranstaltungProfilScreen( + VeranstaltungProfilScreen( veranstalterId = vId, veranstaltungId = evtId, onBack = onBack, onTurnierNeu = { - val veranstaltung = at.mocode.desktop.v2.Store.eventsFor(vId).firstOrNull { it.id == evtId } - val list = at.mocode.desktop.v2.TurnierStore.list(evtId) + val veranstaltung = Store.eventsFor(vId).firstOrNull { it.id == evtId } + val list = TurnierStore.list(evtId) val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L - val draft = at.mocode.desktop.v2.Turnier( + val draft = Turnier( id = newId, veranstaltungId = evtId, turnierNr = 0, datumVon = veranstaltung?.datumVon ?: "", datumBis = veranstaltung?.datumBis, ) - at.mocode.desktop.v2.TurnierStore.add(evtId, draft) + TurnierStore.add(evtId, draft) onNavigate(AppScreen.TurnierDetail(evtId, newId)) }, onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) }, @@ -711,21 +715,21 @@ private fun DesktopContentArea( veranstaltungId = currentScreen.id, onBack = onBack, onTurnierNeu = { - val v = at.mocode.desktop.v2.Store.vereine.firstOrNull { vv -> - at.mocode.desktop.v2.Store.eventsFor(vv.id).any { it.id == currentScreen.id } + val v = Store.vereine.firstOrNull { vv -> + Store.eventsFor(vv.id).any { it.id == currentScreen.id } } val veranstaltung = - v?.let { at.mocode.desktop.v2.Store.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } } - val list = at.mocode.desktop.v2.TurnierStore.list(currentScreen.id) + v?.let { Store.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } } + val list = TurnierStore.list(currentScreen.id) val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L - val draft = at.mocode.desktop.v2.Turnier( + val draft = Turnier( id = newId, veranstaltungId = currentScreen.id, turnierNr = 0, datumVon = veranstaltung?.datumVon ?: "", datumBis = veranstaltung?.datumBis, ) - at.mocode.desktop.v2.TurnierStore.add(currentScreen.id, draft) + TurnierStore.add(currentScreen.id, draft) onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId)) }, onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) }, @@ -739,8 +743,8 @@ private fun DesktopContentArea( // Turnier-Screens is AppScreen.TurnierDetail -> { val evtId = currentScreen.veranstaltungId - val parent = at.mocode.desktop.v2.Store.vereine.firstOrNull { v -> - at.mocode.desktop.v2.Store.eventsFor(v.id).any { it.id == evtId } + val parent = Store.vereine.firstOrNull { v -> + Store.eventsFor(v.id).any { it.id == evtId } } if (parent == null) { InvalidContextNotice( @@ -748,7 +752,7 @@ private fun DesktopContentArea( onBack = onBack ) } 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 bundesland = mapOepsToBundesland(blCode) TurnierDetailScreen( @@ -768,9 +772,8 @@ private fun DesktopContentArea( is AppScreen.TurnierNeu -> { val evtId = currentScreen.veranstaltungId - // V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert - val parent = at.mocode.desktop.v2.Store.vereine.firstOrNull { v -> - at.mocode.desktop.v2.Store.eventsFor(v.id).any { it.id == evtId } + val parent = Store.vereine.firstOrNull { v -> + Store.eventsFor(v.id).any { it.id == evtId } } if (parent == null) { InvalidContextNotice( @@ -778,7 +781,7 @@ private fun DesktopContentArea( onBack = onBack ) } else { - at.mocode.desktop.v2.TurnierWizard( + TurnierWizard( veranstalterId = parent.id, veranstaltungId = evtId, onBack = onBack, @@ -839,7 +842,7 @@ private fun DesktopContentArea( } is AppScreen.NennungsEingang -> { - at.mocode.desktop.v2.NennungsEingangScreen( + NennungsEingangScreen( onBack = onBack ) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/ManagementScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/ManagementScreens.kt similarity index 98% rename from frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/ManagementScreens.kt rename to frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/ManagementScreens.kt index d4f72d23..0b5bd10c 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/ManagementScreens.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/ManagementScreens.kt @@ -1,4 +1,4 @@ -package at.mocode.desktop.v2 +package at.mocode.desktop.screens.management import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -16,6 +16,7 @@ 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.data.Store @Composable fun ManagementTableScreen( @@ -253,8 +254,8 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { @Composable fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) { - // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten - // Wir nutzen hier die 'vereine' Liste aus dem Store. + // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten, + // wir nutzen hier die 'vereine' Liste aus dem Store. val vereine = Store.vereine var filter by remember { mutableStateOf("") } val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt new file mode 100644 index 00000000..57f4e086 --- /dev/null +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt @@ -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(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) } + ) + } + } + } + } + } +} diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/nennung/NennungsEingangScreen.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/nennung/NennungsEingangScreen.kt new file mode 100644 index 00000000..c4c322cb --- /dev/null +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/nennung/NennungsEingangScreen.kt @@ -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>(emptyList()) } + var searchQuery by remember { mutableStateOf("") } + var selectedMail by remember { mutableStateOf(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" + ) +) diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt new file mode 100644 index 00000000..454d7848 --- /dev/null +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt @@ -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)) + } + } + } + } + } + } +} diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/profile/ProfileScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/profile/ProfileScreens.kt new file mode 100644 index 00000000..14edf6eb --- /dev/null +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/profile/ProfileScreens.kt @@ -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()) + } + } + ) + } + } + } +} diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt similarity index 98% rename from frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt rename to frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt index e4278bf0..7ec1dc64 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt @@ -1,4 +1,4 @@ -package at.mocode.desktop.v2 +package at.mocode.desktop.screens.veranstaltung import androidx.compose.foundation.clickable 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.unit.dp 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 org.koin.compose.koinInject import java.time.Instant @@ -43,7 +48,7 @@ fun VeranstaltungVerwaltung( onNavigateToVeranstalter: () -> Unit, onNavigateToZnsImport: () -> Unit ) { - DesktopThemeV2 { + DesktopTheme { val allVeranstaltungen = remember { Store.allEvents() } val vereine = Store.vereine @@ -823,7 +828,7 @@ fun VeranstaltungKonfig( val znsImporter: ZnsImportProvider = koinInject() val znsState = znsImporter.state - DesktopThemeV2 { + DesktopTheme { var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) } var selectedVereinId by remember { mutableStateOf(veranstalterId) } 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 = mutableStateListOf(), - var klassen: SnapshotStateList = mutableStateListOf(), - var kategorie: SnapshotStateList = mutableStateListOf(), - var datumVon: String, - var datumBis: String?, - var titel: String = "", - var subTitel: String = "", - var sponsoren: SnapshotStateList = mutableStateListOf(), -) - -object TurnierStore { - private val map = mutableMapOf>() - fun list(veranstaltungId: Long): MutableList = 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 = map.values.flatten() -} @Composable fun VeranstaltungProfilScreen( @@ -1006,7 +983,7 @@ fun VeranstaltungProfilScreen( onTurnierOpen: (Long) -> Unit, onNavigateToVeranstalterProfil: (Long) -> Unit, ) { - DesktopThemeV2 { + DesktopTheme { val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) } @@ -1228,7 +1205,7 @@ fun TurnierWizard( onBack: () -> Unit, onSaved: (Long) -> Unit, ) { - DesktopThemeV2 { + DesktopTheme { val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } var currentStep by remember { mutableStateOf(1) } var showZnsDialog by remember { mutableStateOf(false) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Theme.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/theme/Theme.kt similarity index 86% rename from frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Theme.kt rename to frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/theme/Theme.kt index 43920664..9225dab2 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Theme.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/theme/Theme.kt @@ -1,9 +1,8 @@ -package at.mocode.desktop.v2 +package at.mocode.desktop.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Typography -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color @@ -26,7 +25,7 @@ private val LightColors: ColorScheme = lightColorScheme( ) @Composable -fun DesktopThemeV2(content: @Composable () -> Unit) { +fun DesktopTheme(content: @Composable () -> Unit) { MaterialTheme( colorScheme = LightColors, typography = Typography(), diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/NennungsEingangScreen.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/NennungsEingangScreen.kt deleted file mode 100644 index 3995c7c6..00000000 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/NennungsEingangScreen.kt +++ /dev/null @@ -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>(emptyList()) } - var searchQuery by remember { mutableStateOf("") } - var selectedMail by remember { mutableStateOf(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") -) diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt deleted file mode 100644 index e24b4cfb..00000000 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt +++ /dev/null @@ -1,1017 +0,0 @@ -package at.mocode.desktop.v2 - -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.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions -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.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import at.mocode.desktop.screens.onboarding.ExpectedClient -import at.mocode.desktop.screens.onboarding.NetworkRole -import at.mocode.desktop.screens.onboarding.OnboardingSettings -import at.mocode.desktop.screens.onboarding.OnboardingValidator -import at.mocode.frontend.core.designsystem.components.MsTextField -import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService -import org.koin.compose.koinInject -import javax.print.PrintServiceLookup -import javax.swing.JFileChooser - -@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() - } - - DesktopThemeV2 { - 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 und synchronisiert Daten.", - style = MaterialTheme.typography.bodySmall - ) - } - } - } - } - - Button( - onClick = { currentStep = 1 }, - modifier = Modifier.align(Alignment.End) - ) { - Text("Weiter") - Icon(Icons.AutoMirrored.Filled.ArrowForward, null, Modifier.padding(start = 8.dp)) - } - } - } - } else { - // PHASE 2: ROLLENSPEZIFISCH - var showPw by remember { mutableStateOf(false) } - val focusManager = LocalFocusManager.current - - // 2.1 / 2.2 IDENTITÄT & SICHERHEIT - Card(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("🛡️ Identität & Sicherheit", style = MaterialTheme.typography.titleMedium) - - if (settings.networkRole == NetworkRole.MASTER) { - MsTextField( - value = settings.geraetName, - onValueChange = { onSettingsChange(settings.copy(geraetName = it)) }, - label = "Gerätename (Pflicht)", - placeholder = "z. B. Haupt-PC", - modifier = Modifier.fillMaxWidth(), - imeAction = ImeAction.Next, - keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }) - ) - } else { - // Client: Auswahlbox - var expanded by remember { mutableStateOf(false) } - val availableSlots = - discoveredServices.flatMap { it.metadata["availableSlots"]?.split(",") ?: emptyList() } - .filter { it.isNotBlank() } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } - ) { - MsTextField( - value = settings.geraetName, - onValueChange = {}, - readOnly = true, - label = "Gerätename (Vom Master freigegeben)", - trailingIcon = Icons.Default.ArrowDropDown, - modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - if (availableSlots.isEmpty()) { - DropdownMenuItem( - text = { Text("Suche nach verfügbaren Slots...") }, - onClick = { expanded = false } - ) - } else { - availableSlots.forEach { slot -> - DropdownMenuItem( - text = { Text(slot) }, - onClick = { - onSettingsChange(settings.copy(geraetName = slot)) - expanded = false - } - ) - } - } - } - } - } - - MsTextField( - value = settings.sharedKey, - onValueChange = { onSettingsChange(settings.copy(sharedKey = it)) }, - label = "Sicherheitsschlüssel (Pflicht)", - placeholder = "Shared Secret für Netzwerk-Sync", - trailingIcon = if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility, - onTrailingIconClick = { showPw = !showPw }, - visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - imeAction = ImeAction.Next, - keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }) - ) - } - } - - // 3.1 ERWARTETE GERÄTE (NUR MASTER) - if (settings.networkRole == NetworkRole.MASTER) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("📋 Erwartete Geräte (Clients)", style = MaterialTheme.typography.titleMedium) - Text( - "Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - settings.expectedClients.forEachIndexed { index, client -> - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - MsTextField( - value = client.name, - onValueChange = { newName -> - val newList = settings.expectedClients.toMutableList() - newList[index] = client.copy(name = newName) - onSettingsChange(settings.copy(expectedClients = newList)) - }, - label = "Name", - modifier = Modifier.weight(1f) - ) - - var expanded by remember { mutableStateOf(false) } - Box { - OutlinedButton(onClick = { expanded = true }) { - Text(client.role.name) - Icon(Icons.Default.ArrowDropDown, null) - } - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - NetworkRole.entries.filter { it != NetworkRole.MASTER }.forEach { role -> - DropdownMenuItem( - text = { Text(role.name) }, - onClick = { - val newList = settings.expectedClients.toMutableList() - newList[index] = client.copy(role = role) - onSettingsChange(settings.copy(expectedClients = newList)) - expanded = false - } - ) - } - } - } - - IconButton(onClick = { - val newList = settings.expectedClients.toMutableList() - newList.removeAt(index) - onSettingsChange(settings.copy(expectedClients = newList)) - }) { - Icon( - Icons.Default.Delete, - contentDescription = "Entfernen", - tint = MaterialTheme.colorScheme.error - ) - } - } - } - - TextButton( - onClick = { - val newList = settings.expectedClients.toMutableList() - newList.add(ExpectedClient("Neues Gerät", NetworkRole.CLIENT)) - onSettingsChange(settings.copy(expectedClients = newList)) - }, - modifier = Modifier.padding(top = 8.dp) - ) { - Icon(Icons.Default.Add, null) - Spacer(Modifier.width(8.dp)) - Text("Gerät hinzufügen") - } - } - } - } - - // 4.1 / 3.2 DATENBANK-SICHERHEITSPFAD - Card(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - if (settings.networkRole == NetworkRole.MASTER) "💾 Datenbank-Sicherheitspfad" else "💾 Lokaler Cache-Sicherungspfad", - style = MaterialTheme.typography.titleMedium - ) - - MsTextField( - value = settings.backupPath, - onValueChange = { onSettingsChange(settings.copy(backupPath = it)) }, - label = "Pfad auswählen", - placeholder = "Verzeichnis für Backups/Cache", - modifier = Modifier.fillMaxWidth(), - trailingIcon = Icons.Default.FolderOpen, - onTrailingIconClick = { - val chooser = JFileChooser() - chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY - chooser.dialogTitle = "Verzeichnis auswählen" - if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { - onSettingsChange(settings.copy(backupPath = chooser.selectedFile.absolutePath)) - } - }, - imeAction = ImeAction.Next, - keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }) - ) - } - } - - // 5.1 SYNC-INTERVALL (NUR MASTER) - if (settings.networkRole == NetworkRole.MASTER) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("🔄 Sync-Intervall", style = MaterialTheme.typography.titleMedium) - Text("📡 Intervall: ${settings.syncInterval} Minuten", style = MaterialTheme.typography.labelLarge) - Slider( - value = settings.syncInterval.toFloat(), - onValueChange = { onSettingsChange(settings.copy(syncInterval = it.toInt())) }, - valueRange = 1f..60f, - steps = 59, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - - // 6.1 / 4.2 DRUCKER - Card(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("🖨️ Drucker", style = MaterialTheme.typography.titleMedium) - - var showPrinterDialog by remember { mutableStateOf(false) } - val availablePrinters = remember { - PrintServiceLookup.lookupPrintServices(null, null).map { it.name } - } - - MsTextField( - value = settings.defaultPrinter, - onValueChange = { onSettingsChange(settings.copy(defaultPrinter = it)) }, - label = "Standard-Drucker", - placeholder = "Name des Standard-Druckers", - modifier = Modifier.fillMaxWidth(), - trailingIcon = Icons.Default.Print, - onTrailingIconClick = { showPrinterDialog = true }, - imeAction = ImeAction.Done, - keyboardActions = KeyboardActions(onDone = { - if (OnboardingValidator.canContinue(settings)) onContinue(settings) - }) - ) - - if (showPrinterDialog) { - AlertDialog( - onDismissRequest = { showPrinterDialog = false }, - title = { Text("Drucker auswählen") }, - text = { - Column(Modifier.verticalScroll(rememberScrollState())) { - if (availablePrinters.isEmpty()) { - Text("Keine Drucker gefunden", style = MaterialTheme.typography.bodyMedium) - } else { - availablePrinters.forEach { printer -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onSettingsChange(settings.copy(defaultPrinter = printer)) - showPrinterDialog = false - } - .padding(vertical = 12.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton(selected = settings.defaultPrinter == printer, onClick = null) - Spacer(Modifier.width(8.dp)) - Text(printer) - } - } - } - } - }, - confirmButton = { TextButton(onClick = { showPrinterDialog = false }) { Text("Schließen") } } - ) - } - } - } - - 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)) - } - } - } - } - } - } -} - -@Preview -@Composable -fun OnboardingScreenPreview() { - var settings by remember { mutableStateOf(OnboardingSettings()) } - OnboardingScreen( - settings = settings, - onSettingsChange = { settings = it }, - onContinue = {} - ) -} - -@Composable -fun PferdProfilV2(id: Long, onBack: () -> Unit) { - DesktopThemeV2 { - val pferd = remember(id) { Store.pferde.firstOrNull { it.id == id } } - if (pferd == null) { - Text("Pferd nicht gefunden"); return@DesktopThemeV2 - } - 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 ReiterProfilV2(id: Long, onBack: () -> Unit) { - DesktopThemeV2 { - val r = remember(id) { Store.reiter.firstOrNull { it.id == id } } - if (r == null) { - Text("Reiter nicht gefunden"); return@DesktopThemeV2 - } - 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(0xFF4B5563), shape = MaterialTheme.shapes.small), - contentAlignment = Alignment.Center - ) { - val initials = - (r.vorname + " " + r.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("${r.vorname} ${r.nachname}", style = MaterialTheme.typography.titleMedium) - val l2 = listOfNotNull( - r.oepsNummer?.let { "OEPS: $it" }, - r.feiId?.let { "FEI: $it" }, - r.lizenzKlasse.takeIf { it.isNotBlank() }).joinToString(" · ") - if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280)) - r.verein?.let { Text(it, 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 fei by remember { mutableStateOf(r.feiId ?: "") } - 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.feiId = fei.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)) - } - 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(liz, { liz = it }, label = "Lizenzklasse", modifier = Modifier.weight(1f)) - MsTextField(verein, { verein = it }, label = "Verein", modifier = Modifier.weight(1f)) - } - } - } - ) - } - } - } -} - -@Composable -fun VereinProfilV2(id: Long, onBack: () -> Unit) { - DesktopThemeV2 { - val v = remember(id) { Store.vereine.firstOrNull { it.id == id } } - if (v == null) { - Text("Verein nicht gefunden"); return@DesktopThemeV2 - } - 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.isNullOrBlank() }.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 ?: "") } - var logo by remember { mutableStateOf(v.logoUrl ?: "") } - - 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 } - v.logoUrl = logo.ifBlank { null } - editOpen = false - }) { Text("Speichern") } - }, - dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } }, - title = { Text("Verein 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 / Adresse") }, - 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)) - } - } - } - ) - } - } - } -} - -@Composable -fun FunktionaerProfilV2(id: Long, onBack: () -> Unit) { - DesktopThemeV2 { - val f = remember(id) { Store.funktionaere.firstOrNull { it.id == id } } - if (f == null) { - Text("Funktionär nicht gefunden"); return@DesktopThemeV2 - } - 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)) { - OutlinedTextField(vor, { vor = it }, label = { Text("Vorname") }, modifier = Modifier.weight(1f)) - OutlinedTextField(nach, { nach = it }, label = { Text("Nachname") }, modifier = Modifier.weight(1f)) - } - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField(num, { num = it }, label = { Text("Nummer") }, modifier = Modifier.weight(1f)) - OutlinedTextField( - qual, - { qual = it }, - label = { Text("Qualifikation") }, - modifier = Modifier.weight(1f) - ) - } - OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.fillMaxWidth()) - } - } - ) - } - } - } -} - -@Composable -fun VeranstalterAuswahlV2( - onBack: () -> Unit, - onWeiter: (Long) -> Unit, - onNeu: () -> Unit, -) { - DesktopThemeV2 { - 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(null) } - - LazyColumn(Modifier.fillMaxSize()) { - items(Store.vereine) { v -> - val sel = selectedId == v.id - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 6.dp) - .clickable { - selectedId = v.id - // Direktnavigation beim Klick auf eine Karte - onWeiter(v.id) - }, - border = if (sel) ButtonDefaults.outlinedButtonBorder(enabled = true) else null, - ) { - Column(Modifier.padding(12.dp)) { - Text(v.name, fontWeight = FontWeight.SemiBold) - Text("OEPS: ${v.oepsNummer} · ${v.ort}", color = Color(0xFF6B7280)) - } - } - } - } - - Button(onClick = { selectedId?.let(onWeiter) }, enabled = selectedId != null) { Text("Weiter zum Veranstalter") } - } - } -} - -@Composable -fun VeranstalterDetailV2( - veranstalterId: Long, - onBack: () -> Unit, - onZurVeranstaltung: (Long) -> Unit, - onNeuVeranstaltung: () -> Unit, -) { - DesktopThemeV2 { - 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() }) - val verein = Store.vereine.firstOrNull { it.id == veranstalterId } - Text(verein?.name ?: "Veranstalter", style = MaterialTheme.typography.titleLarge) - Spacer(Modifier.weight(1f)) - Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") } - } - - // Veranstalter Vorschau-Karte mit Bearbeiten-Dialog - val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } } - if (verein != null) { - var editOpen by remember { mutableStateOf(false) } - Card(Modifier.fillMaxWidth()) { - Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) { - // Logo/Avatar - 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.isNullOrBlank() }.joinToString(" · ") - if (line3.isNotBlank()) Text(line3, color = Color(0xFF6B7280)) - } - Button(onClick = { editOpen = true }) { Text("bearbeiten") } - } - } - - if (editOpen) { - // Lokale Edit-Felder - 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 = { - // Speichern in Store - 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( - value = name, - onValueChange = { name = it }, - label = { Text("Name") }, - modifier = Modifier.fillMaxWidth() - ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField( - value = oeps, - onValueChange = { oeps = it }, - label = { Text("OEPS-Nummer") }, - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = logo, - onValueChange = { logo = it }, - label = { Text("Logo-URL") }, - modifier = Modifier.weight(1f) - ) - } - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField( - value = ort, - onValueChange = { ort = it }, - label = { Text("Ort") }, - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = plz, - onValueChange = { plz = it }, - label = { Text("PLZ") }, - modifier = Modifier.weight(1f) - ) - } - OutlinedTextField( - value = strasse, - onValueChange = { strasse = it }, - label = { Text("Straße / Adresse") }, - modifier = Modifier.fillMaxWidth() - ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("E-Mail") }, - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = tel, - onValueChange = { tel = it }, - label = { Text("Telefon") }, - modifier = Modifier.weight(1f) - ) - } - } - } - ) - } - } - - val events = Store.eventsFor(veranstalterId) - // Filter-/Suchmaske - var search by remember { mutableStateOf("") } - OutlinedTextField( - value = search, - onValueChange = { search = it }, - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - placeholder = { Text("Veranstaltungen suchen…") }, - modifier = Modifier.fillMaxWidth() - ) - - val filtered = remember(events, search) { - val q = search.trim() - if (q.isEmpty()) events else events.filter { - it.titel.contains(q, ignoreCase = true) || - it.status.contains(q, ignoreCase = true) || - it.datumVon.contains(q, ignoreCase = true) || - (it.datumBis?.contains(q, ignoreCase = true) == true) - } - } - if (filtered.isEmpty()) Text("Keine passenden Veranstaltungen gefunden.", color = Color(0xFF6B7280)) - - LazyColumn(Modifier.fillMaxSize()) { - items(filtered) { evt -> - Card(Modifier.fillMaxWidth().padding(vertical = 6.dp)) { - Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) { - Column(Modifier.weight(1f)) { - Text(evt.titel, fontWeight = FontWeight.SemiBold) - Text("${evt.datumVon}${evt.datumBis?.let { " – $it" } ?: ""}", color = Color(0xFF6B7280)) - AssistChip(onClick = {}, label = { Text(evt.status) }) - } - Button(onClick = { onZurVeranstaltung(evt.id) }) { Text("Zur Veranstaltung") } - Spacer(Modifier.width(8.dp)) - var confirm by remember { mutableStateOf(false) } - if (confirm) { - AlertDialog( - onDismissRequest = { confirm = false }, - confirmButton = { - TextButton(onClick = { - Store.removeEvent(veranstalterId, evt.id) - confirm = false - }) { Text("Löschen") } - }, - dismissButton = { TextButton(onClick = { confirm = false }) { Text("Abbrechen") } }, - title = { Text("Veranstaltung löschen?") }, - text = { Text("Diese Aktion entfernt die Veranstaltung und alle zugehörigen Turniere im Prototypen.") } - ) - } - IconButton(onClick = { confirm = true }) { - Icon( - Icons.Default.Delete, - contentDescription = "Löschen", - tint = Color(0xFFDC2626) - ) - } - } - } - } - } - } - } -}