refactor(desktop): Alte Verwaltungsscreens entfernt und Code reduziert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
parent
3949ab21db
commit
8857d52f44
|
|
@ -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.
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package at.mocode.desktop.data
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
|
||||
data class Turnier(
|
||||
val id: Long,
|
||||
val veranstaltungId: Long,
|
||||
val turnierNr: Int,
|
||||
var typ: String = "ÖTO (National)",
|
||||
var znsDataLoaded: Boolean = false,
|
||||
var sparten: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var klassen: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var kategorie: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var datumVon: String,
|
||||
var datumBis: String?,
|
||||
var titel: String = "",
|
||||
var subTitel: String = "",
|
||||
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
|
||||
)
|
||||
|
||||
object TurnierStore {
|
||||
private val map = mutableMapOf<Long, MutableList<Turnier>>()
|
||||
fun list(veranstaltungId: Long): MutableList<Turnier> = map.getOrPut(veranstaltungId) { mutableListOf() }
|
||||
fun add(veranstaltungId: Long, t: Turnier) {
|
||||
list(veranstaltungId).add(0, t)
|
||||
}
|
||||
|
||||
fun remove(veranstaltungId: Long, tId: Long) {
|
||||
list(veranstaltungId).removeAll { it.id == tId }
|
||||
}
|
||||
|
||||
// Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab)
|
||||
@JvmStatic
|
||||
fun allTurniere(): List<Turnier> = map.values.flatten()
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <T> 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 {
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
package at.mocode.desktop.screens.management
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.data.Store
|
||||
import at.mocode.desktop.theme.DesktopTheme
|
||||
|
||||
@Composable
|
||||
fun VeranstalterAuswahl(
|
||||
onBack: () -> Unit,
|
||||
onWeiter: (Long) -> Unit,
|
||||
onNeu: () -> Unit,
|
||||
) {
|
||||
DesktopTheme {
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text("Veranstalter auswählen", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(onClick = onNeu) { Text("+ Neuer Veranstalter") }
|
||||
}
|
||||
|
||||
var selectedId by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize().weight(1f)) {
|
||||
items(Store.vereine) { v ->
|
||||
val sel = selectedId == v.id
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
.clickable { selectedId = v.id },
|
||||
colors = if (sel) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
else CardDefaults.cardColors()
|
||||
) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier.size(40.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text((v.kurzname ?: v.name).take(2).uppercase(), color = Color.White)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(v.name, style = MaterialTheme.typography.titleMedium)
|
||||
Text("OEPS: ${v.oepsNummer} · ${v.ort ?: ""}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { selectedId?.let(onWeiter) },
|
||||
enabled = selectedId != null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("Weiter zum Veranstalter") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VeranstalterDetail(
|
||||
veranstalterId: Long,
|
||||
onBack: () -> Unit,
|
||||
onZurVeranstaltung: (Long) -> Unit,
|
||||
onNeuVeranstaltung: () -> Unit,
|
||||
) {
|
||||
DesktopTheme {
|
||||
val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } }
|
||||
|
||||
if (verein == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Veranstalter nicht gefunden")
|
||||
}
|
||||
return@DesktopTheme
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text(verein.name, style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
|
||||
}
|
||||
|
||||
var editOpen by remember { mutableStateOf(false) }
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
(verein.kurzname ?: verein.name).take(2).uppercase(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(verein.name, style = MaterialTheme.typography.titleMedium)
|
||||
val line2 = listOfNotNull(
|
||||
"OEPS: ${verein.oepsNummer}",
|
||||
verein.ort,
|
||||
verein.plz,
|
||||
verein.strasse
|
||||
).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
if (line2.isNotBlank()) Text(line2, color = Color(0xFF6B7280))
|
||||
val line3 = listOfNotNull(verein.email, verein.telefon).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
if (line3.isNotBlank()) Text(line3, color = Color(0xFF6B7280))
|
||||
}
|
||||
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
|
||||
}
|
||||
}
|
||||
|
||||
if (editOpen) {
|
||||
var name by remember { mutableStateOf(verein.name) }
|
||||
var oeps by remember { mutableStateOf(verein.oepsNummer) }
|
||||
var ort by remember { mutableStateOf(verein.ort ?: "") }
|
||||
var plz by remember { mutableStateOf(verein.plz ?: "") }
|
||||
var strasse by remember { mutableStateOf(verein.strasse ?: "") }
|
||||
var email by remember { mutableStateOf(verein.email ?: "") }
|
||||
var tel by remember { mutableStateOf(verein.telefon ?: "") }
|
||||
var logo by remember { mutableStateOf(verein.logoUrl ?: "") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { editOpen = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
verein.name = name
|
||||
verein.oepsNummer = oeps
|
||||
verein.ort = ort.ifBlank { null }
|
||||
verein.plz = plz.ifBlank { null }
|
||||
verein.strasse = strasse.ifBlank { null }
|
||||
verein.email = email.ifBlank { null }
|
||||
verein.telefon = tel.ifBlank { null }
|
||||
verein.logoUrl = logo.ifBlank { null }
|
||||
editOpen = false
|
||||
}) { Text("Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
|
||||
title = { Text("Veranstalter bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("OEPS-Nummer") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(logo, { logo = it }, label = { Text("Logo-URL") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(ort, { ort = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(plz, { plz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
OutlinedTextField(
|
||||
strasse,
|
||||
{ strasse = it },
|
||||
label = { Text("Straße") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(tel, { tel = it }, label = { Text("Telefon") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text("Veranstaltungen", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
|
||||
val events = remember(veranstalterId) { Store.eventsFor(veranstalterId) }
|
||||
if (events.isEmpty()) {
|
||||
Text("Keine Veranstaltungen angelegt", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||
} else {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(events) { ev ->
|
||||
ListItem(
|
||||
headlineContent = { Text(ev.titel) },
|
||||
supportingContent = { Text("${ev.datumVon} bis ${ev.datumBis ?: "?"} · ${ev.status}") },
|
||||
trailingContent = { Icon(Icons.Default.ChevronRight, null) },
|
||||
modifier = Modifier.clickable { onZurVeranstaltung(ev.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
package at.mocode.desktop.screens.nennung
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.desktop.theme.DesktopTheme
|
||||
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
|
||||
import at.mocode.frontend.features.nennung.domain.NennungResponse
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
data class OnlineNennungMail(
|
||||
val id: String,
|
||||
val sender: String,
|
||||
val empfaenger: String,
|
||||
val datum: String,
|
||||
val turnierNr: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferd: String,
|
||||
val pferdAlter: String,
|
||||
val telefon: String?,
|
||||
val bewerbe: String,
|
||||
val bemerkungen: String?,
|
||||
var status: String = "NEU"
|
||||
)
|
||||
|
||||
fun NennungResponse.toMail() = OnlineNennungMail(
|
||||
id = id,
|
||||
sender = email,
|
||||
empfaenger = "Meldestelle",
|
||||
datum = "-", // Datum ist in Entity nicht direkt drin, könnte man ergänzen
|
||||
turnierNr = turnierNr,
|
||||
vorname = vorname,
|
||||
nachname = nachname,
|
||||
lizenz = lizenz,
|
||||
pferd = pferdName,
|
||||
pferdAlter = pferdAlter,
|
||||
telefon = telefon,
|
||||
bewerbe = bewerbe,
|
||||
bemerkungen = bemerkungen,
|
||||
status = if (status == "GELESEN") "GELESEN" else "NEU"
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NennungsEingangScreen(onBack: () -> Unit) {
|
||||
val repository: NennungRemoteRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DesktopTheme {
|
||||
var mails by remember { mutableStateOf<List<OnlineNennungMail>>(emptyList()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var selectedMail by remember { mutableStateOf<OnlineNennungMail?>(null) }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
val refresh = {
|
||||
scope.launch {
|
||||
isRefreshing = true
|
||||
repository.holeNennungen().onSuccess { response ->
|
||||
mails = response.map { it.toMail() }
|
||||
}.onFailure {
|
||||
// Fallback oder Fehleranzeige
|
||||
if (mails.isEmpty()) mails = getMockMails()
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
val filteredMails = remember(mails, searchQuery) {
|
||||
if (searchQuery.isBlank()) mails
|
||||
else mails.filter {
|
||||
it.vorname.contains(searchQuery, ignoreCase = true) ||
|
||||
it.nachname.contains(searchQuery, ignoreCase = true) ||
|
||||
it.pferd.contains(searchQuery, ignoreCase = true) ||
|
||||
it.turnierNr.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
// Initiales Laden
|
||||
LaunchedEffect(Unit) {
|
||||
refresh()
|
||||
}
|
||||
|
||||
if (selectedMail != null) {
|
||||
NennungDetailDialog(
|
||||
mail = selectedMail!!,
|
||||
onDismiss = { selectedMail = null },
|
||||
onMarkProcessed = {
|
||||
scope.launch {
|
||||
repository.markiereAlsGelesen(selectedMail!!.id)
|
||||
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
|
||||
mails = updated
|
||||
selectedMail = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) }
|
||||
Icon(Icons.Default.Email, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
Text("Nennungs-Eingang (Online-Nennen)", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
Button(
|
||||
onClick = { refresh() },
|
||||
enabled = !isRefreshing
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Aktualisieren")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
"Hier werden alle eingegangenen Online-Nennungen angezeigt. Klicke auf 'Anzeigen', um alle Details für die manuelle Übernahme zu sehen.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
// Suchfeld
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Suche nach Reiter, Pferd oder Turnier-Nr...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, null) },
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
// Tabelle
|
||||
Card(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
||||
Column {
|
||||
// Header Zeile
|
||||
Row(
|
||||
Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant).padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Status", Modifier.width(80.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Datum", Modifier.width(150.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Turnier", Modifier.width(80.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Reiter", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Pferd", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Bewerbe", Modifier.weight(1f), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Aktion", Modifier.width(120.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (filteredMails.isEmpty() && !isRefreshing) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
if (searchQuery.isBlank()) "Keine neuen Nennungen vorhanden."
|
||||
else "Keine Nennungen für '$searchQuery' gefunden.",
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
items(filteredMails) { mail ->
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Badge(
|
||||
containerColor = if (mail.status == "NEU") Color(0xFFE74C3C) else Color(0xFFBDC3C7),
|
||||
modifier = Modifier.width(80.dp).padding(end = 8.dp)
|
||||
) {
|
||||
Text(mail.status, color = Color.White, fontSize = 10.sp)
|
||||
}
|
||||
Text(mail.datum, Modifier.width(150.dp), fontSize = 13.sp)
|
||||
Text(mail.turnierNr, Modifier.width(80.dp), fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("${mail.vorname} ${mail.nachname}", Modifier.width(200.dp), fontSize = 13.sp)
|
||||
Text(mail.pferd, Modifier.width(200.dp), fontSize = 13.sp)
|
||||
Text(mail.bewerbe, Modifier.weight(1f), fontSize = 13.sp)
|
||||
|
||||
Button(
|
||||
onClick = { selectedMail = mail },
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||
modifier = Modifier.width(120.dp).height(32.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Text("Anzeigen", fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(Modifier.padding(horizontal = 8.dp), thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkProcessed: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Details zur Online-Nennung") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
DetailRow("Absender", mail.sender)
|
||||
DetailRow("Turnier", mail.turnierNr)
|
||||
DetailRow("Eingang", mail.datum)
|
||||
HorizontalDivider()
|
||||
Text("Reiter: ${mail.vorname} ${mail.nachname} (${mail.lizenz})", fontWeight = FontWeight.Bold)
|
||||
Text("Pferd: ${mail.pferd} (Geb. ${mail.pferdAlter})", fontWeight = FontWeight.Bold)
|
||||
DetailRow("Telefon", mail.telefon ?: "-")
|
||||
HorizontalDivider()
|
||||
Text("Ausgewählte Bewerbe:", fontWeight = FontWeight.SemiBold)
|
||||
Text(mail.bewerbe)
|
||||
if (!mail.bemerkungen.isNullOrBlank()) {
|
||||
Text("Bemerkungen:", fontWeight = FontWeight.SemiBold)
|
||||
Text(mail.bemerkungen, color = Color.DarkGray)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onMarkProcessed) { Text("Als gelesen markieren") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Schließen") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailRow(label: String, value: String) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text("$label: ", fontWeight = FontWeight.SemiBold, modifier = Modifier.width(100.dp))
|
||||
Text(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMockMails() = listOf(
|
||||
OnlineNennungMail(
|
||||
"1",
|
||||
"max.mustermann@web.de",
|
||||
"meldestelle-26128@mo-code.at",
|
||||
"14.04.2026 14:30",
|
||||
"26128",
|
||||
"Max",
|
||||
"Mustermann",
|
||||
"R2",
|
||||
"Spirit",
|
||||
"2015",
|
||||
"0664/1234567",
|
||||
"1, 2, 5",
|
||||
"Brauche Box für Freitag"
|
||||
),
|
||||
OnlineNennungMail(
|
||||
"2",
|
||||
"susi.sorglos@gmx.at",
|
||||
"meldestelle-26128@mo-code.at",
|
||||
"14.04.2026 15:12",
|
||||
"26128",
|
||||
"Susi",
|
||||
"Sorglos",
|
||||
"LF",
|
||||
"Flocke",
|
||||
"2018",
|
||||
null,
|
||||
"10, 11",
|
||||
null
|
||||
),
|
||||
OnlineNennungMail(
|
||||
"3",
|
||||
"info@reitstall-hofer.at",
|
||||
"meldestelle-26129@mo-code.at",
|
||||
"14.04.2026 16:05",
|
||||
"26129",
|
||||
"Georg",
|
||||
"Hofer",
|
||||
"R3",
|
||||
"Black Beauty",
|
||||
"2012",
|
||||
"0676/9876543",
|
||||
"3, 4, 8",
|
||||
"Bitte späte Startzeit"
|
||||
)
|
||||
)
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> = mutableStateListOf(),
|
||||
var klassen: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var kategorie: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var datumVon: String,
|
||||
var datumBis: String?,
|
||||
var titel: String = "",
|
||||
var subTitel: String = "",
|
||||
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
|
||||
)
|
||||
|
||||
object TurnierStore {
|
||||
private val map = mutableMapOf<Long, MutableList<Turnier>>()
|
||||
fun list(veranstaltungId: Long): MutableList<Turnier> = map.getOrPut(veranstaltungId) { mutableListOf() }
|
||||
fun add(veranstaltungId: Long, t: Turnier) {
|
||||
list(veranstaltungId).add(0, t)
|
||||
}
|
||||
fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } }
|
||||
|
||||
// Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab)
|
||||
@JvmStatic
|
||||
fun allTurniere(): List<Turnier> = map.values.flatten()
|
||||
}
|
||||
|
||||
@Composable
|
||||
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) }
|
||||
|
|
@ -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(),
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
package at.mocode.desktop.v2
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
|
||||
import at.mocode.frontend.features.nennung.domain.NennungResponse
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
data class OnlineNennungMail(
|
||||
val id: String,
|
||||
val sender: String,
|
||||
val empfaenger: String,
|
||||
val datum: String,
|
||||
val turnierNr: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferd: String,
|
||||
val pferdAlter: String,
|
||||
val telefon: String?,
|
||||
val bewerbe: String,
|
||||
val bemerkungen: String?,
|
||||
var status: String = "NEU"
|
||||
)
|
||||
|
||||
fun NennungResponse.toMail() = OnlineNennungMail(
|
||||
id = id,
|
||||
sender = email,
|
||||
empfaenger = "Meldestelle",
|
||||
datum = "-", // Datum ist in Entity nicht direkt drin, könnte man ergänzen
|
||||
turnierNr = turnierNr,
|
||||
vorname = vorname,
|
||||
nachname = nachname,
|
||||
lizenz = lizenz,
|
||||
pferd = pferdName,
|
||||
pferdAlter = pferdAlter,
|
||||
telefon = telefon,
|
||||
bewerbe = bewerbe,
|
||||
bemerkungen = bemerkungen,
|
||||
status = if (status == "GELESEN") "GELESEN" else "NEU"
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NennungsEingangScreen(onBack: () -> Unit) {
|
||||
val repository: NennungRemoteRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DesktopThemeV2 {
|
||||
var mails by remember { mutableStateOf<List<OnlineNennungMail>>(emptyList()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var selectedMail by remember { mutableStateOf<OnlineNennungMail?>(null) }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
val refresh = {
|
||||
scope.launch {
|
||||
isRefreshing = true
|
||||
repository.holeNennungen().onSuccess { response ->
|
||||
mails = response.map { it.toMail() }
|
||||
}.onFailure {
|
||||
// Fallback oder Fehleranzeige
|
||||
if (mails.isEmpty()) mails = getMockMails()
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
val filteredMails = remember(mails, searchQuery) {
|
||||
if (searchQuery.isBlank()) mails
|
||||
else mails.filter {
|
||||
it.vorname.contains(searchQuery, ignoreCase = true) ||
|
||||
it.nachname.contains(searchQuery, ignoreCase = true) ||
|
||||
it.pferd.contains(searchQuery, ignoreCase = true) ||
|
||||
it.turnierNr.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
// Initiales Laden
|
||||
LaunchedEffect(Unit) {
|
||||
refresh()
|
||||
}
|
||||
|
||||
if (selectedMail != null) {
|
||||
NennungDetailDialog(
|
||||
mail = selectedMail!!,
|
||||
onDismiss = { selectedMail = null },
|
||||
onMarkProcessed = {
|
||||
scope.launch {
|
||||
repository.markiereAlsGelesen(selectedMail!!.id)
|
||||
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
|
||||
mails = updated
|
||||
selectedMail = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) }
|
||||
Icon(Icons.Default.Email, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
Text("Nennungs-Eingang (Online-Nennen)", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
Button(
|
||||
onClick = { refresh() },
|
||||
enabled = !isRefreshing
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Aktualisieren")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
"Hier werden alle eingegangenen Online-Nennungen angezeigt. Klicke auf 'Anzeigen', um alle Details für die manuelle Übernahme zu sehen.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
// Suchfeld
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Suche nach Reiter, Pferd oder Turnier-Nr...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, null) },
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
// Tabelle
|
||||
Card(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
||||
Column {
|
||||
// Header Zeile
|
||||
Row(
|
||||
Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceVariant).padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Status", Modifier.width(80.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Datum", Modifier.width(150.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Turnier", Modifier.width(80.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Reiter", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Pferd", Modifier.width(200.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Bewerbe", Modifier.weight(1f), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
Text("Aktion", Modifier.width(120.dp), fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (filteredMails.isEmpty() && !isRefreshing) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
if (searchQuery.isBlank()) "Keine neuen Nennungen vorhanden."
|
||||
else "Keine Nennungen für '$searchQuery' gefunden.",
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
items(filteredMails) { mail ->
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Badge(
|
||||
containerColor = if (mail.status == "NEU") Color(0xFFE74C3C) else Color(0xFFBDC3C7),
|
||||
modifier = Modifier.width(80.dp).padding(end = 8.dp)
|
||||
) {
|
||||
Text(mail.status, color = Color.White, fontSize = 10.sp)
|
||||
}
|
||||
Text(mail.datum, Modifier.width(150.dp), fontSize = 13.sp)
|
||||
Text(mail.turnierNr, Modifier.width(80.dp), fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("${mail.vorname} ${mail.nachname}", Modifier.width(200.dp), fontSize = 13.sp)
|
||||
Text(mail.pferd, Modifier.width(200.dp), fontSize = 13.sp)
|
||||
Text(mail.bewerbe, Modifier.weight(1f), fontSize = 13.sp)
|
||||
|
||||
Button(
|
||||
onClick = { selectedMail = mail },
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||
modifier = Modifier.width(120.dp).height(32.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Text("Anzeigen", fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(Modifier.padding(horizontal = 8.dp), thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkProcessed: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Details zur Online-Nennung") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
DetailRow("Absender", mail.sender)
|
||||
DetailRow("Turnier", mail.turnierNr)
|
||||
DetailRow("Eingang", mail.datum)
|
||||
HorizontalDivider()
|
||||
Text("Reiter: ${mail.vorname} ${mail.nachname} (${mail.lizenz})", fontWeight = FontWeight.Bold)
|
||||
Text("Pferd: ${mail.pferd} (Geb. ${mail.pferdAlter})", fontWeight = FontWeight.Bold)
|
||||
DetailRow("Telefon", mail.telefon ?: "-")
|
||||
HorizontalDivider()
|
||||
Text("Ausgewählte Bewerbe:", fontWeight = FontWeight.SemiBold)
|
||||
Text(mail.bewerbe)
|
||||
if (!mail.bemerkungen.isNullOrBlank()) {
|
||||
Text("Bemerkungen:", fontWeight = FontWeight.SemiBold)
|
||||
Text(mail.bemerkungen, color = Color.DarkGray)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onMarkProcessed) { Text("Als gelesen markieren") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Schließen") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailRow(label: String, value: String) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text("$label: ", fontWeight = FontWeight.SemiBold, modifier = Modifier.width(100.dp))
|
||||
Text(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMockMails() = listOf(
|
||||
OnlineNennungMail("1", "max.mustermann@web.de", "meldestelle-26128@mo-code.at", "14.04.2026 14:30", "26128", "Max", "Mustermann", "R2", "Spirit", "2015", "0664/1234567", "1, 2, 5", "Brauche Box für Freitag"),
|
||||
OnlineNennungMail("2", "susi.sorglos@gmx.at", "meldestelle-26128@mo-code.at", "14.04.2026 15:12", "26128", "Susi", "Sorglos", "LF", "Flocke", "2018", null, "10, 11", null),
|
||||
OnlineNennungMail("3", "info@reitstall-hofer.at", "meldestelle-26129@mo-code.at", "14.04.2026 16:05", "26129", "Georg", "Hofer", "R3", "Black Beauty", "2012", "0676/9876543", "3, 4, 8", "Bitte späte Startzeit")
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user