diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index 9f637857..06a73a68 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -186,18 +186,30 @@ und über definierte Schnittstellen kommunizieren. ## 4. Geplante Phasen -### PHASE 7: Desktop-Vernetzung & Event-First Workflow 🔵 IN ARBEIT +### PHASE 7: Desktop-Vernetzung & Zentrale Verwaltung ✅ ABGESCHLOSSEN -*Ziel: LAN-Kommunikation zwischen Apps und Fokus auf Veranstaltungs-Verwaltung.* +*Ziel: LAN-Kommunikation Vorbereitung und Etablierung der "Veranstaltung-Verwaltung" als zentrale Schaltstelle.* +* [x] **Zentrale Verwaltung:** Etablierung der `Veranstaltung-Verwaltung` (Zentrale) als administratives Cockpit. +* [x] **Navigation:** Implementierung eines Back-Stack-Systems für intelligente "Zurück"-Navigation. +* [x] **Domänen-Synchronisation:** Anpassung der Frontend-Stores an die Backend-Masterdata-Modelle (Reiter, Pferde, + Vereine, Funktionäre). +* [x] **ZNS-Integration (Frontend):** ZNS-Importer in die Zentrale integriert; Konzept "Globaler Pool -> Lokale + Synchronisation" gefestigt. +* [x] **Terminologie:** UI-weit Umstellung von "Event" auf "Veranstaltung" (ÖTO-konform). * [x] **Konzept:** LAN-Discovery (mDNS) und Echtzeit-Sync (WebSockets) entworfen. * [x] **ADR:** ADR-0020 (Lokale Netzwerk-Kommunikation) erstellt. -* [ ] **Discovery:** Implementierung des mDNS-Service für die Geräte-Suche. -* [ ] **Transport:** Aufbau der WebSocket-Infrastruktur für P2P-Sync. -* [ ] **Event-First:** Umstellung des App-Startpunkts auf die Veranstaltungs-Liste (Login-Skip). -* [ ] **Wizard:** Implementierung des `VeranstaltungNeuWizard` zur Neuanlage. -### PHASE 8: Series-Context & Erweiterungen 🔵 PHASE 2+ +### PHASE 8: Bewerbe-Management & Startlisten 🔵 IN ARBEIT + +*Ziel: Fachliche Tiefe in den Turnieren (Import, Generierung, Zeitberechnung).* + +* [ ] **Bewerbe-Import:** Implementierung der Merge-Logik (ZNS-XML -> BewerbUiModel). +* [ ] **Startlisten-Automatisierung:** Generierung und Zeitberechnung (Pausen, Umbauzeiten). +* [ ] **Discovery:** Implementierung des mDNS-Service für die Geräte-Suche (Phase 7 Übertrag). +* [ ] **Transport:** Aufbau der WebSocket-Infrastruktur für P2P-Sync (Phase 7 Übertrag). + +### PHASE 9: Series-Context & Erweiterungen 🔵 PHASE 2+ *Ziel: Cups, Serien und Meisterschaften mit konfigurierbaren Reglements.* diff --git a/docs/06_Frontend/screen-flow_1-04-26.md b/docs/06_Frontend/screen-flow_1-04-26.md new file mode 100644 index 00000000..4fb5a760 --- /dev/null +++ b/docs/06_Frontend/screen-flow_1-04-26.md @@ -0,0 +1,75 @@ +### Diagramm (Screen-Flow) - Stand 01. April 2026 (Abend-Update) + +Das Frontend nutzt nun einen **Back-Stack Mechanism**, d.h. der "Zurück"-Button (TopBar) führt immer zum jeweils +vorherigen Screen im Verlauf. + +```mermaid +graph TD + A[Onboarding] -- " Login / Start " --> H[Veranstaltung-Verwaltung] + + subgraph "Verwaltung (Zentrale)" + H + H1[Pferde-Verwaltung] + H2[Reiter-Verwaltung] + H3[Verein-Verwaltung] + H4[Funktionär-Verwaltung] + H5[Veranstalter-Verwaltung] + H6[ZNS-Importer] + end + + H -- " Pferde " --> H1 + H -- " Reiter " --> H2 + H -- " Vereine " --> H3 + H -- " Funktionäre " --> H4 + H -- " Veranstalter " --> H5 + H -- " ZNS-Importer " --> H6 + H6 -- " Back-Stack " --> H + H1 -- " Pferde-Profil " --> P1[Pferde-Profil] + H2 -- " Reiter-Profil " --> P2[Reiter-Profil] + H3 -- " Verein-Profil " --> P3[Verein-Profil] + H4 -- " Funktionär-Profil " --> P4[Funktionär-Profil] + H5 -- " Pferde-Profil " --> P1 + P1 -- " Back-Stack " --> H + P2 -- " Back-Stack " --> H + P3 -- " Back-Stack " --> H + P4 -- " Back-Stack " --> H + H -- " Neue Veranstaltung " --> E[VeranstaltungKonfigV2] + H -- " Veranstaltung öffnen " --> F[Veranstaltung-Profil] + F -- " Turnier öffnen / Neu " --> G[TurnierDetailScreen] + F -- " Veranstalter-Profil " --> H5 + + subgraph "TurnierDetailScreen (Tabs)" + G1[STAMMDATEN] + G2[ORGANISATION] + G3[BEWERBE] + G4[ARTIKEL] + G5[ABRECHNUNG] + G6[NENNUNGEN] + G7[STARTLISTEN] + G8[ERGEBNISLISTEN] + end + + G --> G1 + G --> G3 + G --> G7 + F -- " Back-Stack " --> H + H5 -- " Back-Stack " --> F + H -- " Logout / Zurück " --> A + + subgraph "Legacy / Administrative Screens" + L1[AdminUebersichtScreen] + L2[PingScreen] + L3[ProfileScreen] + L4[VereinScreen] + L5[VeranstaltungDetailScreen] + end + + H -- " Ping " --> L2 + H -- " Profil " --> L3 + H -- " Vereine " --> L4 + H -- " Legacy View " --> L1 +%% Fallback navigation + L1 -- " Veranstalter Auswahl " --> H + L1 -- " Legacy Event Detail " --> L5 + L5 -- " Turnier öffnen " --> G +``` diff --git a/docs/06_Frontend/uebersicht-v01.png b/docs/06_Frontend/uebersicht-v01.png new file mode 100644 index 00000000..c012280d Binary files /dev/null and b/docs/06_Frontend/uebersicht-v01.png differ diff --git a/docs/06_Frontend/uebersicht-v02.png b/docs/06_Frontend/uebersicht-v02.png new file mode 100644 index 00000000..e85331ea Binary files /dev/null and b/docs/06_Frontend/uebersicht-v02.png differ diff --git a/docs/06_Frontend/verwaltung_01-04-26.png b/docs/06_Frontend/verwaltung_01-04-26.png new file mode 100644 index 00000000..79f98d98 Binary files /dev/null and b/docs/06_Frontend/verwaltung_01-04-26.png differ diff --git a/docs/99_Journal/2026-04-01_Abschluss_Vormittag_Zentrale_Dokumentation.md b/docs/99_Journal/2026-04-01_Abschluss_Vormittag_Zentrale_Dokumentation.md new file mode 100644 index 00000000..5a5c351b --- /dev/null +++ b/docs/99_Journal/2026-04-01_Abschluss_Vormittag_Zentrale_Dokumentation.md @@ -0,0 +1,51 @@ +# 🧹 Curator – Session Log (2026-04-01 - Abschluss Vormittag) + +## Zusammenfassung + +Die heutige Vormittags-Sitzung markiert den Übergang von einer "Sammlung von Screens" zu einer integrierten **Zentralen +Verwaltung**. Wir haben das "Chaos" im Frontend durch eine klare Hierarchie und einen intelligenten Navigations-Flow ( +Back-Stack) gezähmt. + +## Kern-Erkenntnisse & Architektur-Updates + +### 1. Die "Zentrale" als Cockpit + +- Die `Veranstaltung-Verwaltung` ist nun der primäre Einstiegspunkt nach dem Onboarding. +- Alle administrativen Domänen (Pferde, Reiter, Vereine, Funktionäre, Veranstalter) sind über diese Zentrale erreichbar. +- Dies entspricht dem Wunsch des Users nach einer "Haupt-Verwaltungs-Zentrale" (`verwaltung_01-04-26.png`). + +### 2. ZNS-Datenfluss: Globaler Pool -> Lokale Synchronisation + +- **Konzept:** Stammdaten (ZNS) werden global in die Desktop-App geladen (ZNS-Importer). +- **Vorteil:** Turniere müssen Daten nicht mehr isoliert halten, sondern synchronisieren sich selektiv mit dem globalen + Pool über den "Aktualisieren"-Button. +- Die Domänen-Modelle in `Stores.kt` wurden vollständig an die Backend-Modelle (`DomPferd`, `DomReiter`, etc.) + angepasst. + +### 3. Navigations-Logik (Back-Stack) + +- Implementierung von `navigateBack()` im `DesktopNavigationPort`. +- Der "Zurück"-Button merkt sich nun den Pfad (z.B. Veranstaltung-Profil -> Veranstalter-Profil -> Zurück -> + Veranstaltung-Profil). + +### 4. Terminologie-Bereinigung + +- UI-weit wurde "Event" durch **"Veranstaltung"** ersetzt (ÖTO-konform). +- Technische IDs behalten den Namen `eventId` zur Stabilität. + +## Durchgeführte Code-Änderungen (Zusammenfassung) + +- `AppScreen.kt`: Neue Routen für Verwaltungen und Profile. +- `ManagementScreens.kt`: Generische Tabellen-Komponente mit Suche und CRUD-Aktionen. +- `Stores.kt`: Erweiterte Datenklassen und realistische Testdaten. +- `DesktopMainLayout.kt`: Integration des Back-Stacks und der neuen Routen. +- `VeranstaltungScreens.kt`: Umbenennung in `VeranstaltungProfilScreen` und Header-Updates. + +## Status der Dokumentation + +- [x] **Screen-Flow:** `docs/06_Frontend/screen-flow_1-04-26.md` aktualisiert. +- [x] **Journal:** Einzelergebnisse in separaten Logs dokumentiert. +- [x] **Roadmap:** `MASTER_ROADMAP.md` auf Phase 8 (Bewerbe-Management) aktualisiert. + +--- +*Dokumentiert durch den Curator am 01.04.2026 um 17:40 Uhr.* diff --git a/docs/99_Journal/2026-04-01_Session_Log_Zentrale_ZNS.md b/docs/99_Journal/2026-04-01_Session_Log_Zentrale_ZNS.md new file mode 100644 index 00000000..595ddb2c --- /dev/null +++ b/docs/99_Journal/2026-04-01_Session_Log_Zentrale_ZNS.md @@ -0,0 +1,41 @@ +# Session Log: 01. April 2026 - Vormittag (Zentrale & ZNS-Logik) + +## 🏗️ [Lead Architect] | Status & Entscheidungen + +### 1. Die "Zentrale" (Veranstaltung-Verwaltung) + +Wir haben die **Veranstaltung-Verwaltung** als neue strategische Zentrale etabliert. Von hier aus sind alle +administrativen Bereiche (Pferde, Reiter, Vereine, Funktionäre, Veranstalter) erreichbar. Dies löst das "Chaos" im +Frontend durch eine klare Hierarchie. + +### 2. ZNS-Datenfluss: Global -> Lokal + +Ein entscheidendes Architektur-Konzept wurde heute Vormittag gefestigt: + +* **Globaler Pool:** ZNS-Stammdaten (Pferde, Personen, Vereine) werden über den ZNS-Importer in die globale Datenbank + der Desktop-App geladen. +* **Lokale Synchronisation:** In den Turnier-Details (z.B. `TurnierBewerbeTab`) dient der Button **"Aktualisieren"** + dazu, die Daten für dieses spezifische Turnier mit dem globalen Pool abzugleichen. +* **Vorteil:** Daten müssen nicht pro Turnier neu importiert werden. Ein globaler Stand (z.B. nach einem ZNS-Update) + kann selektiv in die aktiven Turniere "gepusht" werden. + +### 3. Terminologie-Bereinigung + +Alle UI-Texte wurden auf **"Veranstaltung"** umgestellt, um konform mit der ÖTO (§ 2 Abs. 1) zu sein. "Event" bleibt ein +technischer Begriff im Code. + +## 👷 [Backend/Frontend] | Durchgeführte Änderungen + +* **App-Routing:** `AppScreen.kt` um neue Verwaltungs-Routen erweitert. +* **Navigation:** `DesktopMainLayout.kt` implementiert nun den Flow von der Zentrale in die Fachbereiche. +* **Importer-Integration:** Der ZNS-Importer ist nun direkt aus der Zentrale erreichbar. +* **Bugfix:** Kompilierfehler in der Navigation (fehlender `onBack` Parameter) behoben. + +## 🧐 [QA Specialist] | Offene Punkte für den Nachmittag + +* [ ] **Bewerbe-Import:** Implementierung der konkreten Merge-Logik (ZNS-XML -> `BewerbUiModel`). +* [ ] **Startlisten-Sortierung:** Validierung der ÖTO-konformen Auslosung. +* [ ] **Profil-Screens:** Die Placeholder für Pferde-, Reiter- etc. Profile müssen mit Leben gefüllt werden. + +--- +*Dokumentiert durch den Curator am 01.04.2026* diff --git a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt index d2b5229b..2a5f16f2 100644 --- a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt +++ b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt @@ -18,7 +18,25 @@ sealed class AppScreen(val route: String) { data object Nennung : AppScreen("/nennung") // --- Desktop-Navigation (Vision_03) --- - data object Veranstaltungen : AppScreen("/veranstaltungen") + data object VeranstaltungVerwaltung : AppScreen("/verwaltung") // Gesamtübersicht + + // Profile + data object PferdVerwaltung : AppScreen("/pferde/verwaltung") + data class PferdProfil(val id: Long) : AppScreen("/pferde/profil/$id") + + data object ReiterVerwaltung : AppScreen("/reiter/verwaltung") + data class ReiterProfil(val id: Long) : AppScreen("/reiter/profil/$id") + + data object VereinVerwaltung : AppScreen("/vereine/verwaltung") + data class VereinProfil(val id: Long) : AppScreen("/vereine/profil/$id") + + data object FunktionaerVerwaltung : AppScreen("/funktionaere/verwaltung") + data class FunktionaerProfil(val id: Long) : AppScreen("/funktionaere/profil/$id") + + data object VeranstalterVerwaltung : AppScreen("/veranstalter/verwaltung") + data class VeranstalterProfil(val id: Long) : AppScreen("/veranstalter/profil/$id") + + // data class VeranstaltungProfil(val id: Long) : AppScreen("/veranstaltung/profil/$id") // Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl") @@ -28,7 +46,7 @@ sealed class AppScreen(val route: String) { // Neue Veranstaltungs-Konfig-Seite (aus Veranstalter-Detail oder direkt aus Cockpit) data class VeranstaltungKonfig(val veranstalterId: Long = 0) : AppScreen("/veranstalter/$veranstalterId/veranstaltung/neu") - data class VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) : + data class VeranstaltungProfil(val veranstalterId: Long, val veranstaltungId: Long) : AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId") data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id") @@ -51,7 +69,14 @@ sealed class AppScreen(val route: String) { private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$") private val VERANSTALTER_DETAIL = Regex("/veranstalter/(\\d+)$") private val VERANSTALTUNG_KONFIG = Regex("/veranstalter/(\\d+)/veranstaltung/neu$") - private val VERANSTALTUNG_UEBERSICHT = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$") + private val VERANSTALTUNG_PROFIL = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$") + + private val PFERD_PROFIL = Regex("/pferde/profil/(\\d+)$") + private val REITER_PROFIL = Regex("/reiter/profil/(\\d+)$") + private val VEREIN_PROFIL = Regex("/vereine/profil/(\\d+)$") + private val FUNKTIONAER_PROFIL = Regex("/funktionaere/profil/(\\d+)$") + private val VERANSTALTER_PROFIL = Regex("/veranstalter/profil/(\\d+)$") + // private val VERANSTALTUNG_PROFIL_LEGACY = Regex("/veranstaltung/profil/(\\d+)$") fun fromRoute(route: String): AppScreen { return when (route) { @@ -66,17 +91,27 @@ sealed class AppScreen(val route: String) { "/organizer/profile" -> OrganizerProfile "/auth/callback" -> AuthCallback "/nennung" -> Nennung - "/veranstaltungen" -> Veranstaltungen + "/verwaltung" -> VeranstaltungVerwaltung + "/pferde/verwaltung" -> PferdVerwaltung + "/reiter/verwaltung" -> ReiterVerwaltung + "/vereine/verwaltung" -> VereinVerwaltung + "/funktionaere/verwaltung" -> FunktionaerVerwaltung + "/veranstalter/verwaltung" -> VeranstalterVerwaltung "/veranstalter/auswahl" -> VeranstalterAuswahl "/veranstaltung/neu" -> VeranstaltungNeu - "/reiter" -> Reiter - "/pferde" -> Pferde - "/vereine" -> Vereine - "/funktionaere" -> Funktionaere "/meisterschaften" -> Meisterschaften "/cups" -> Cups "/stammdaten/import" -> StammdatenImport else -> { + PFERD_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return PferdProfil(id.toLong()) } + REITER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return ReiterProfil(id.toLong()) } + VEREIN_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VereinProfil(id.toLong()) } + FUNKTIONAER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return FunktionaerProfil(id.toLong()) } + VERANSTALTER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstalterProfil(id.toLong()) } + /* + VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstaltungProfil(id.toLong()) } + */ + TURNIER_DETAIL.matchEntire(route)?.destructured?.let { (vId, tId) -> return TurnierDetail(vId.toLong(), tId.toLong()) } @@ -92,8 +127,8 @@ sealed class AppScreen(val route: String) { VERANSTALTUNG_KONFIG.matchEntire(route)?.destructured?.let { (vId) -> return VeranstaltungKonfig(vId.toLong()) } - VERANSTALTUNG_UEBERSICHT.matchEntire(route)?.destructured?.let { (verId, vId) -> - return VeranstaltungUebersicht(verId.toLong(), vId.toLong()) + VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) -> + return VeranstaltungProfil(verId.toLong(), vId.toLong()) } Landing // Default fallback } diff --git a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/NavigationPort.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/NavigationPort.kt index 3f851ae8..eda149b5 100644 --- a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/NavigationPort.kt +++ b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/NavigationPort.kt @@ -15,4 +15,7 @@ interface NavigationPort { /** Typsichere Navigation direkt via AppScreen-Objekt. */ fun navigateToScreen(screen: AppScreen) + + /** Gehe einen Schritt zurück im Verlauf. */ + fun navigateBack() } diff --git a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt index 174f5e7f..bd56112b 100644 --- a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt +++ b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt @@ -6,6 +6,7 @@ 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.* import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -21,6 +22,7 @@ import javax.swing.filechooser.FileNameExtensionFilter @Composable fun StammdatenImportScreen( viewModel: ZnsImportViewModel = koinViewModel(), + onBack: () -> Unit, ) { val state = viewModel.state @@ -32,6 +34,9 @@ fun StammdatenImportScreen( ) { // Titel Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } Icon(Icons.Default.CloudUpload, contentDescription = null, tint = MaterialTheme.colorScheme.primary) Text("Stammdaten-Import (ZNS)", style = MaterialTheme.typography.headlineSmall) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt index da9c023f..52df7415 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt @@ -37,16 +37,17 @@ fun DesktopApp() { val authState by authTokenManager.authState.collectAsState() // Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt - // Vision_03 Update: Wir starten direkt in der Veranstaltungs-Übersicht (Offline-First) - if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Veranstaltungen + // Vision_03 Update: Wir starten mit Onboarding + if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Onboarding + && currentScreen !is AppScreen.VeranstaltungVerwaltung && currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu && currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig - && currentScreen !is AppScreen.VeranstaltungUebersicht && currentScreen !is AppScreen.TurnierDetail - && currentScreen !is AppScreen.TurnierNeu && currentScreen !is AppScreen.Vereine + && currentScreen !is AppScreen.VeranstaltungProfil && currentScreen !is AppScreen.TurnierDetail + && currentScreen !is AppScreen.TurnierNeu ) { LaunchedEffect(Unit) { - // Standard: Direkt zur Veranstaltungs-Übersicht (Offline-First-Modus) - nav.navigateToScreen(AppScreen.Veranstaltungen) + // Standard: Start im Onboarding + nav.navigateToScreen(AppScreen.Onboarding) } } @@ -54,7 +55,7 @@ fun DesktopApp() { is AppScreen.Login -> LoginScreen( viewModel = loginViewModel, onLoginSuccess = { - val returnTo = screen.returnTo ?: AppScreen.Veranstaltungen + val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung nav.navigateToScreen(returnTo) }, onBack = { /* Desktop hat keine Landing-Page */ }, @@ -65,9 +66,10 @@ fun DesktopApp() { DesktopMainLayout( currentScreen = screen, onNavigate = { nav.navigateToScreen(it) }, + onBack = { nav.navigateBack() }, onLogout = { authTokenManager.clearToken() - nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Veranstaltungen)) + nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung)) }, ) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/navigation/DesktopNavigationPort.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/navigation/DesktopNavigationPort.kt index 89d68c3e..efd96767 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/navigation/DesktopNavigationPort.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/navigation/DesktopNavigationPort.kt @@ -14,14 +14,35 @@ class DesktopNavigationPort : NavigationPort { private val _currentScreen = MutableStateFlow(AppScreen.Onboarding) override val currentScreen: StateFlow = _currentScreen.asStateFlow() + // Backstack zur Speicherung des Verlaufs + private val backStack = mutableListOf() + override fun navigateTo(route: String) { val screen = AppScreen.fromRoute(route) println("[DesktopNav] navigateTo $route -> $screen") - _currentScreen.value = screen + navigateToScreen(screen) } override fun navigateToScreen(screen: AppScreen) { println("[DesktopNav] navigateToScreen -> $screen") + // Aktuellen Screen auf den Stack legen, falls er nicht derselbe ist + val current = _currentScreen.value + if (current != screen) { + backStack.add(current) + // Begrenzung des Backstacks auf z.B. 50 Einträge + if (backStack.size > 50) backStack.removeAt(0) + } _currentScreen.value = screen } + + override fun navigateBack() { + if (backStack.isNotEmpty()) { + val previousScreen = backStack.removeAt(backStack.size - 1) + println("[DesktopNav] navigateBack -> $previousScreen") + _currentScreen.value = previousScreen + } else { + println("[DesktopNav] navigateBack -> Stack leer, bleibe bei Onboarding") + _currentScreen.value = AppScreen.Onboarding + } + } } 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 420be3fc..2427269a 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 @@ -5,14 +5,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Chat +import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.filled.Devices import androidx.compose.material.icons.filled.Wifi import androidx.compose.material.icons.filled.WifiOff -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -56,12 +53,14 @@ private val TopBarTextColor = Color.White fun DesktopMainLayout( currentScreen: AppScreen, onNavigate: (AppScreen) -> Unit, + onBack: () -> Unit, onLogout: () -> Unit, ) { Column(modifier = Modifier.fillMaxSize()) { DesktopTopBar( currentScreen = currentScreen, onNavigate = onNavigate, + onBack = onBack, onLogout = onLogout, ) Column(modifier = Modifier.fillMaxSize()) { @@ -69,6 +68,7 @@ fun DesktopMainLayout( DesktopContentArea( currentScreen = currentScreen, onNavigate = onNavigate, + onBack = onBack, ) } DesktopFooterBar() @@ -89,6 +89,7 @@ fun DesktopMainLayout( private fun DesktopTopBar( currentScreen: AppScreen, onNavigate: (AppScreen) -> Unit, + onBack: () -> Unit, onLogout: () -> Unit, ) { Row( @@ -102,25 +103,25 @@ private fun DesktopTopBar( ) { Row(verticalAlignment = Alignment.CenterVertically) { // Zurück-Pfeil (nur wenn nicht Root) - if (currentScreen !is AppScreen.Veranstaltungen) { + if (currentScreen !is AppScreen.VeranstaltungVerwaltung) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück", tint = TopBarTextColor, modifier = Modifier .size(20.dp) - .clickable { onNavigate(AppScreen.Veranstaltungen) }, + .clickable { onBack() }, ) Spacer(Modifier.width(8.dp)) } // Root-Link Text( - text = "Veranstaltungen", + text = "Verwaltung", color = TopBarTextColor, fontSize = 14.sp, fontWeight = FontWeight.Medium, - modifier = Modifier.clickable { onNavigate(AppScreen.Veranstaltungen) }, + modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) }, ) // Breadcrumb-Segmente je nach Screen @@ -166,7 +167,7 @@ private fun DesktopTopBar( fontWeight = FontWeight.SemiBold, ) } - is AppScreen.VeranstaltungUebersicht -> { + is AppScreen.VeranstaltungProfil -> { BreadcrumbSeparator() Text( text = "Veranstalter auswählen", @@ -305,6 +306,27 @@ private fun InvalidContextNotice(message: String, onBack: () -> Unit) { } } +@Composable +fun PlaceholderScreen( + title: String, + onBack: () -> Unit, + onAction: (() -> Unit)? = null, + actionLabel: String = "Aktion ausführen" +) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text(title, style = MaterialTheme.typography.headlineMedium) + Text("Dieser Screen ist noch in Arbeit (Placeholder)", color = Color.Gray) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = onBack) { Text("Zurück") } + if (onAction != null) { + Button(onClick = onAction) { Text(actionLabel) } + } + } + } + } +} + /** * Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen. */ @@ -312,49 +334,133 @@ private fun InvalidContextNotice(message: String, onBack: () -> Unit) { private fun DesktopContentArea( currentScreen: AppScreen, onNavigate: (AppScreen) -> Unit, + onBack: () -> Unit, ) { when (currentScreen) { // Onboarding ohne Login is AppScreen.Onboarding -> { val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject() - // V2 Onboarding (Vision_03) - at.mocode.desktop.v2.OnboardingScreenV2 { _, _ -> + at.mocode.desktop.v2.OnboardingScreen { _, _ -> authTokenManager.setToken("dummy.jwt.token") - onNavigate(AppScreen.VeranstalterAuswahl) + onNavigate(AppScreen.VeranstaltungVerwaltung) } } - // Root-Screen: Leitet in V2-Fluss ab - is AppScreen.Veranstaltungen -> { - at.mocode.desktop.v2.VeranstaltungenUebersichtV2( - onEventOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, eId)) }, - onNewEvent = { onNavigate(AppScreen.VeranstaltungKonfig()) } + // Haupt-Zentrale: Veranstaltung-Verwaltung + is AppScreen.VeranstaltungVerwaltung -> { + at.mocode.desktop.v2.VeranstaltungVerwaltungV2( + onVeranstaltungOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }, + onNewVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig()) }, + onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) }, + onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) }, + onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) }, + onNavigateToFunktionaere = { onNavigate(AppScreen.FunktionaerVerwaltung) }, + onNavigateToVeranstalter = { onNavigate(AppScreen.VeranstalterVerwaltung) }, + onNavigateToZnsImport = { onNavigate(AppScreen.StammdatenImport) } ) } + // --- ZNS Importer --- + is AppScreen.StammdatenImport -> { + at.mocode.zns.feature.presentation.StammdatenImportScreen( + onBack = onBack + ) + } + + // --- Pferde-Verwaltung & Profil --- + is AppScreen.PferdVerwaltung -> at.mocode.desktop.v2.PferdeVerwaltungScreen( + onBack = onBack, + onEdit = { onNavigate(AppScreen.PferdProfil(it)) } + ) + + is AppScreen.PferdProfil -> PlaceholderScreen( + "Pferde-Profil #${currentScreen.id}", + onBack = onBack, + onAction = { onNavigate(AppScreen.VeranstaltungVerwaltung) }, + actionLabel = "Zurück zur Zentrale" + ) + + // --- Reiter-Verwaltung & Profil --- + is AppScreen.ReiterVerwaltung -> at.mocode.desktop.v2.ReiterVerwaltungScreen( + onBack = onBack, + onEdit = { onNavigate(AppScreen.ReiterProfil(it)) } + ) + + is AppScreen.ReiterProfil -> PlaceholderScreen( + "Reiter-Profil #${currentScreen.id}", + onBack = onBack, + onAction = { onNavigate(AppScreen.VeranstaltungVerwaltung) }, + actionLabel = "Zurück zur Zentrale" + ) + + // --- Verein-Verwaltung & Profil --- + is AppScreen.VereinVerwaltung -> at.mocode.desktop.v2.VereinVerwaltungScreen( + onBack = onBack, + onEdit = { onNavigate(AppScreen.VereinProfil(it)) } + ) + + is AppScreen.VereinProfil -> PlaceholderScreen( + "Verein-Profil #${currentScreen.id}", + onBack = onBack, + onAction = { onNavigate(AppScreen.VereinVerwaltung) }, + actionLabel = "Zurück zur Zentrale" + ) + + // --- Funktionaer-Verwaltung & Profil --- + is AppScreen.FunktionaerVerwaltung -> at.mocode.desktop.v2.FunktionaerVerwaltungScreen( + onBack = onBack, + onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) } + ) + + is AppScreen.FunktionaerProfil -> PlaceholderScreen( + "Funktionär-Profil #${currentScreen.id}", + onBack = onBack, + onAction = { onNavigate(AppScreen.FunktionaerVerwaltung) }, + actionLabel = "Zurück zur Zentrale" + ) + + // --- Veranstalter-Verwaltung & Profil --- + is AppScreen.VeranstalterVerwaltung -> at.mocode.desktop.v2.VeranstalterVerwaltungScreen( + onBack = onBack, + onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) } + ) + + is AppScreen.VeranstalterProfil -> PlaceholderScreen( + "Veranstalter-Profil #${currentScreen.id}", + onBack = onBack, + onAction = { onNavigate(AppScreen.PferdProfil(1L)) }, + actionLabel = "Pferde-Profil öffnen" + ) + + /* + is AppScreen.VeranstaltungProfil -> PlaceholderScreen("Veranstaltung-Profil #${currentScreen.id}", + onBack = { onNavigate(AppScreen.VeranstaltungVerwaltung) } + ) + */ + // Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2( - onBack = { onNavigate(AppScreen.Veranstaltungen) }, + onBack = onBack, onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, ) is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen( - onAbbrechen = { onNavigate(AppScreen.VeranstalterAuswahl) }, - onSpeichern = { _, _, _ -> onNavigate(AppScreen.VeranstalterAuswahl) }, + onAbbrechen = onBack, + onSpeichern = { _, _, _ -> onBack() }, ) is AppScreen.VeranstalterDetail -> { val vId = currentScreen.veranstalterId if (!FakeVeranstalterStore.exists(vId)) { InvalidContextNotice( message = "Veranstalter (ID=$vId) nicht gefunden.", - onBack = { onNavigate(AppScreen.VeranstalterAuswahl) } + onBack = onBack ) } else { at.mocode.desktop.v2.VeranstalterDetailV2( veranstalterId = vId, - onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }, - onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) }, + onBack = onBack, + onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) }, onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) }, ) } @@ -364,32 +470,30 @@ private fun DesktopContentArea( // Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard at.mocode.desktop.v2.VeranstaltungKonfigV2( veranstalterId = vId, - onBack = { - if (vId == 0L) onNavigate(AppScreen.Veranstaltungen) - else onNavigate(AppScreen.VeranstalterDetail(vId)) - }, - onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) }, + onBack = onBack, + onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) }, onVeranstalterCreated = { newVId -> onNavigate(AppScreen.VeranstalterDetail(newVId)) } ) } - is AppScreen.VeranstaltungUebersicht -> { + + is AppScreen.VeranstaltungProfil -> { val vId = currentScreen.veranstalterId val evtId = currentScreen.veranstaltungId if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) { InvalidContextNotice( message = "Veranstalter (ID=$vId) nicht gefunden.", - onBack = { onNavigate(AppScreen.VeranstalterAuswahl) } + onBack = onBack ) } else if (at.mocode.desktop.v2.StoreV2.eventsFor(vId).none { it.id == evtId }) { InvalidContextNotice( message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.", - onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) } + onBack = onBack ) } else { - at.mocode.desktop.v2.VeranstaltungUebersichtV2( + at.mocode.desktop.v2.VeranstaltungProfilScreen( veranstalterId = vId, veranstaltungId = evtId, - onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }, + onBack = onBack, onTurnierNeu = { val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(vId).firstOrNull { it.id == evtId } val list = at.mocode.desktop.v2.TurnierStoreV2.list(evtId) @@ -405,6 +509,7 @@ private fun DesktopContentArea( onNavigate(AppScreen.TurnierDetail(evtId, newId)) }, onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) }, + onNavigateToVeranstalterProfil = { verId -> onNavigate(AppScreen.VeranstalterProfil(verId)) } ) } } @@ -412,7 +517,7 @@ private fun DesktopContentArea( // Veranstaltungs-Screens is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen( veranstaltungId = currentScreen.id, - onBack = { onNavigate(AppScreen.Veranstaltungen) }, + onBack = onBack, onTurnierNeu = { val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv -> at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id } @@ -433,8 +538,8 @@ private fun DesktopContentArea( onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) }, ) is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen( - onBack = { onNavigate(AppScreen.Veranstaltungen) }, - onSave = { onNavigate(AppScreen.Veranstaltungen) }, + onBack = onBack, + onSave = { onBack() }, ) // Turnier-Screens @@ -446,7 +551,7 @@ private fun DesktopContentArea( if (parent == null) { InvalidContextNotice( message = "Veranstaltung (ID=$evtId) nicht gefunden.", - onBack = { onNavigate(AppScreen.Veranstaltungen) } + onBack = onBack ) } else { val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(parent.id).firstOrNull { it.id == evtId } @@ -455,7 +560,7 @@ private fun DesktopContentArea( TurnierDetailScreen( veranstaltungId = evtId, turnierId = currentScreen.turnierId, - onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) }, + onBack = onBack, eventVon = veranstaltung?.datumVon, eventBis = veranstaltung?.datumBis, eventOrt = veranstaltung?.ort, @@ -475,14 +580,14 @@ private fun DesktopContentArea( if (parent == null) { InvalidContextNotice( message = "Veranstaltung (ID=$evtId) nicht gefunden.", - onBack = { onNavigate(AppScreen.Veranstaltungen) } + onBack = onBack ) } else { at.mocode.desktop.v2.TurnierWizardV2( veranstalterId = parent.id, veranstaltungId = evtId, - onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) }, - onSaved = { _ -> onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) }, + onBack = onBack, + onSaved = { _ -> onBack() }, ) } } @@ -492,7 +597,7 @@ private fun DesktopContentArea( val pingViewModel: PingViewModel = koinInject() PingScreen( viewModel = pingViewModel, - onBack = { onNavigate(AppScreen.Veranstaltungen) }, + onBack = onBack, ) } @@ -556,7 +661,7 @@ private fun DesktopFooterBar() { Row(verticalAlignment = Alignment.CenterVertically) { if (deviceConnected.value) { OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { - Icon(Icons.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB)) + Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB)) Spacer(Modifier.width(6.dp)) Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp) } 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/v2/ManagementScreens.kt new file mode 100644 index 00000000..6efe6f2c --- /dev/null +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/ManagementScreens.kt @@ -0,0 +1,276 @@ +package at.mocode.desktop.v2 + +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.filled.* +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 + +@Composable +fun ManagementTableScreen( + title: String, + items: List, + columns: List>, + onBack: () -> Unit, + onNew: () -> Unit, + onEdit: (T) -> Unit, + onDelete: (T) -> Unit, + onSearch: (String) -> Unit = {} +) { + var searchQuery by remember { mutableStateOf("") } + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + // Header + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = onBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Zurück") + } + Text(title, style = MaterialTheme.typography.headlineMedium) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = searchQuery, + onValueChange = { + searchQuery = it + onSearch(it) + }, + placeholder = { Text("Suchen...") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + modifier = Modifier.width(300.dp).padding(end = 16.dp), + singleLine = true + ) + Button(onClick = onNew) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text("Neu anlegen") + } + } + } + + Spacer(Modifier.height(16.dp)) + + // Tabelle + Card(modifier = Modifier.fillMaxWidth().weight(1f)) { + Column(modifier = Modifier.fillMaxSize()) { + // Table Header + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = 2.dp + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + columns.forEach { col -> + Text( + text = col.header, + modifier = if (col.weight != null) Modifier.weight(col.weight) else Modifier.width(col.width ?: 150.dp), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold + ) + } + Spacer(Modifier.width(100.dp)) // Platz für Aktionen + } + } + + Divider() + + // Table Body + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(items) { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onEdit(item) } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + columns.forEach { col -> + Text( + text = col.cellValue(item), + modifier = if (col.weight != null) Modifier.weight(col.weight) else Modifier.width( + col.width ?: 150.dp + ), + style = MaterialTheme.typography.bodyMedium + ) + } + + // Aktionen + Row(modifier = Modifier.width(100.dp), horizontalArrangement = Arrangement.End) { + IconButton(onClick = { onEdit(item) }) { + Icon(Icons.Default.Edit, contentDescription = "Bearbeiten", tint = MaterialTheme.colorScheme.primary) + } + if (title != "Veranstalter-Verwaltung") { + IconButton(onClick = { onDelete(item) }) { + Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error) + } + } + } + } + Divider() + } + } + } + } + } +} + +data class TableColumn( + val header: String, + val cellValue: (T) -> String, + val width: androidx.compose.ui.unit.Dp? = null, + val weight: Float? = null +) + +@Composable +fun PferdeVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { + val pferde = StoreV2.pferde + var filter by remember { mutableStateOf("") } + val filteredItems = if (filter.isEmpty()) pferde else pferde.filter { + it.name.contains(filter, ignoreCase = true) || it.feiId?.contains(filter, ignoreCase = true) == true + } + + ManagementTableScreen( + title = "Pferde-Verwaltung", + items = filteredItems, + columns = listOf( + TableColumn("Name", { it.name }, weight = 1.5f), + TableColumn("ÖPS-Nr.", { it.oepsNummer ?: "-" }, width = 100.dp), + TableColumn("FEI-ID", { it.feiId ?: "-" }, width = 100.dp), + TableColumn("Lebensnr.", { it.lebensnummer ?: "-" }, width = 150.dp), + TableColumn("Geschl.", { it.geschlecht }, width = 80.dp), + TableColumn("Farbe", { it.farbe ?: "-" }, width = 100.dp), + TableColumn("Geb.Datum", { it.geburtsdatum ?: "-" }, width = 100.dp), + TableColumn("Besitzer", { it.besitzer ?: "-" }, weight = 1f) + ), + onBack = onBack, + onNew = { /* CRUD Logik */ }, + onEdit = { onEdit(it.id) }, + onDelete = { StoreV2.pferde.remove(it) }, + onSearch = { filter = it } + ) +} + +@Composable +fun ReiterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { + val reiter = StoreV2.reiter + var filter by remember { mutableStateOf("") } + val filteredItems = if (filter.isEmpty()) reiter else reiter.filter { + it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains( + filter, + ignoreCase = true + ) || it.oepsNummer?.contains(filter, ignoreCase = true) == true + } + + ManagementTableScreen( + title = "Reiter-Verwaltung", + items = filteredItems, + columns = listOf( + TableColumn("Name", { "${it.vorname} ${it.nachname}" }, weight = 1.5f), + TableColumn("ÖPS-Nr.", { it.oepsNummer ?: "-" }, width = 100.dp), + TableColumn("Lizenz", { it.lizenzKlasse }, width = 100.dp), + TableColumn("Startk.", { if (it.startkartAktiv) "Ja (${it.startkartSaison})" else "Nein" }, width = 100.dp), + TableColumn("Verein", { it.verein ?: "-" }, weight = 1.5f), + TableColumn("Nation", { it.nation }, width = 80.dp) + ), + onBack = onBack, + onNew = { }, + onEdit = { onEdit(it.id) }, + onDelete = { StoreV2.reiter.remove(it) }, + onSearch = { filter = it } + ) +} + +@Composable +fun VereinVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { + val vereine = StoreV2.vereine + var filter by remember { mutableStateOf("") } + val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { + it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true) + } + + ManagementTableScreen( + title = "Verein-Verwaltung", + items = filteredItems, + columns = listOf( + TableColumn("Name", { it.name }, weight = 2f), + TableColumn("ÖPS-Nr.", { it.oepsNummer }, width = 100.dp), + TableColumn("BL", { it.bundesland ?: "-" }, width = 60.dp), + TableColumn("Ort", { it.ort ?: "-" }, weight = 1f), + TableColumn("Veranst.", { if (it.istVeranstalter) "Ja" else "Nein" }, width = 80.dp) + ), + onBack = onBack, + onNew = { }, + onEdit = { onEdit(it.id) }, + onDelete = { StoreV2.vereine.remove(it) }, + onSearch = { filter = it } + ) +} + +@Composable +fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { + val funktionaere = StoreV2.funktionaere + var filter by remember { mutableStateOf("") } + val filteredItems = if (filter.isEmpty()) funktionaere else funktionaere.filter { + it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains(filter, ignoreCase = true) + } + + ManagementTableScreen( + title = "Funktionär-Verwaltung", + items = filteredItems, + columns = listOf( + TableColumn("Name", { "${it.vorname} ${it.nachname}" }, weight = 1.5f), + TableColumn("Nr.", { it.richterNummer ?: "-" }, width = 100.dp), + TableColumn("Rollen", { it.rollen.joinToString(", ") }, weight = 1.2f), + TableColumn("Quali", { it.richterQualifikation ?: "-" }, width = 120.dp), + TableColumn("Sparten", { it.qualifiziertFuerSparten.joinToString(", ") }, weight = 1.2f) + ), + onBack = onBack, + onNew = { }, + onEdit = { onEdit(it.id) }, + onDelete = { StoreV2.funktionaere.remove(it) }, + onSearch = { filter = it } + ) +} + +@Composable +fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { + // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten + // Wir nutzen hier die 'vereine' Liste aus dem Store. + val vereine = StoreV2.vereine + var filter by remember { mutableStateOf("") } + val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { + it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true) + } + + ManagementTableScreen( + title = "Veranstalter-Verwaltung", + items = filteredItems, + columns = listOf( + TableColumn("Name", { it.name }, weight = 2f), + TableColumn("ÖPS-Nr.", { it.oepsNummer }, width = 100.dp), + TableColumn("Ort", { it.ort ?: "-" }, weight = 1f), + TableColumn("BL", { it.bundesland ?: "-" }, width = 60.dp), + TableColumn("Email", { it.email ?: "-" }, weight = 1f) + ), + onBack = onBack, + onNew = { }, + onEdit = { onEdit(it.id) }, + onDelete = { }, + onSearch = { filter = it } + ) +} 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 index 678667ff..77985491 100644 --- 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 @@ -20,7 +20,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @Composable -fun OnboardingScreenV2(onContinue: (String, String) -> Unit) { +fun OnboardingScreen(onContinue: (String, String) -> Unit) { DesktopThemeV2 { Surface(color = MaterialTheme.colorScheme.background) { Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { @@ -139,9 +139,9 @@ fun VeranstalterDetailV2( ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( - value = verein.ansprechpartner ?: "", - onValueChange = { verein.ansprechpartner = it.ifBlank { null } }, - label = { Text("Ansprechpartner (optional)") }, + value = verein.ort ?: "", + onValueChange = { verein.ort = it.ifBlank { null } }, + label = { Text("Ansprechpartner / Ort (optional)") }, modifier = Modifier.weight(1f) ) OutlinedTextField( @@ -166,9 +166,9 @@ fun VeranstalterDetailV2( ) } OutlinedTextField( - value = verein.adresse ?: "", - onValueChange = { verein.adresse = it.ifBlank { null } }, - label = { Text("Adresse (optional)") }, + value = verein.strasse ?: "", + onValueChange = { verein.strasse = it.ifBlank { null } }, + label = { Text("Adresse / Straße (optional)") }, modifier = Modifier.fillMaxWidth(), minLines = 2 ) 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/v2/Stores.kt index 10fab167..266d0724 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/v2/Stores.kt @@ -7,13 +7,68 @@ data class Verein( val id: Long, var name: String, var oepsNummer: String, - var ort: String, - // Profil-Felder (minimal laut Abstimmung) - var logoUrl: String? = null, - var ansprechpartner: String? = null, + var kurzname: String? = null, + var bundesland: String? = null, + var ort: String? = null, + var plz: String? = null, + var strasse: String? = null, var email: String? = null, var telefon: String? = null, - var adresse: String? = null, + var website: String? = null, + var istVeranstalter: Boolean = false, + var logoUrl: String? = null, + var bemerkungen: String? = null, +) + +data class Pferd( + val id: Long, + var name: String, + var geschlecht: String = "Stute", + var geburtsdatum: String? = null, + var rasse: String? = null, + var farbe: String? = null, + var lebensnummer: String? = null, + var chipNummer: String? = null, + var passNummer: String? = null, + var oepsNummer: String? = null, + var feiId: String? = null, + var vater: String? = null, + var mutter: String? = null, + var mutterVater: String? = null, + var stockmass: Int? = null, + var besitzer: String? = null, + var istAktiv: Boolean = true, +) + +data class Reiter( + val id: Long, + var vorname: String, + var nachname: String, + var satznummer: String? = null, + var oepsNummer: String? = null, + var feiId: String? = null, + var lizenzKlasse: String = "LIZENZFREI", + var startkartAktiv: Boolean = false, + var startkartSaison: Int? = null, + var geburtsdatum: String? = null, + var vereinsNummer: String? = null, + var verein: String? = null, + var nation: String = "AUT", + var istGastreiter: Boolean = false, +) + +data class Funktionaer( + val id: Long, + var vorname: String, + var nachname: String, + var richterNummer: String? = null, + var rollen: List = emptyList(), + var richterQualifikation: String? = null, + var qualifiziertFuerSparten: List = emptyList(), + var email: String? = null, + var telefon: String? = null, + var vereinsNummer: String? = null, + var istAktiv: Boolean = true, ) data class VeranstaltungV2( @@ -31,23 +86,185 @@ data class VeranstaltungV2( ) object StoreV2 { + val pferde: SnapshotStateList = mutableStateListOf( + Pferd( + id = 1, + name = "Don Johnson", + feiId = "104FE22", + geschlecht = "Wallach", + farbe = "Fuchs", + vater = "Don Frederico", + mutter = "Waikiki", + geburtsdatum = "2001-01-01", + besitzer = "Isabell Werth", + lebensnummer = "DE 431316694401", + oepsNummer = "3H66" + ), + Pferd( + id = 2, + name = "Bella Rose", + feiId = "103RW04", + geschlecht = "Stute", + farbe = "Fuchs", + vater = "Belissimo M", + mutter = "Cadra II", + geburtsdatum = "2004-01-01", + besitzer = "Madeleine Winter-Schulze", + lebensnummer = "DE 443434443904", + oepsNummer = "2T15" + ), + Pferd( + id = 3, + name = "Valegro", + feiId = "102UB51", + geschlecht = "Wallach", + farbe = "Brauner", + vater = "Negro", + mutter = "Maifleur", + geburtsdatum = "2002-01-01", + besitzer = "Carl Hester & Roly Luard", + lebensnummer = "NLD003NL0204840", + oepsNummer = "1V51" + ), + Pferd( + id = 4, + name = "Dalera BB", + feiId = "104UD89", + geschlecht = "Stute", + farbe = "Brauner", + vater = "Easy Game", + mutter = "Dark Magic", + geburtsdatum = "2007-01-01", + besitzer = "Beatrice Bürchler-Keller", + lebensnummer = "DE 409090124007", + oepsNummer = "4U89" + ), + ) + + val reiter: SnapshotStateList = mutableStateListOf( + Reiter( + id = 1, + vorname = "Isabell", + nachname = "Werth", + oepsNummer = "O-12345", + feiId = "10011469", + verein = "RFV Graf von Schmettow Eversael", + lizenzKlasse = "RD4", + startkartAktiv = true, + startkartSaison = 2026, + nation = "GER" + ), + Reiter( + id = 2, + vorname = "Jessica", + nachname = "von Bredow-Werndl", + oepsNummer = "O-54321", + feiId = "10019075", + verein = "RFV Aubenhausen", + lizenzKlasse = "RD4", + startkartAktiv = true, + startkartSaison = 2026, + nation = "GER" + ), + Reiter( + id = 3, + vorname = "Charlotte", + nachname = "Dujardin", + oepsNummer = "GB-9999", + feiId = "10028445", + verein = "Rowallan Activity Centre", + lizenzKlasse = "RD4", + startkartAktiv = true, + startkartSaison = 2026, + nation = "GBR" + ), + Reiter( + id = 4, + vorname = "Stefan", + nachname = "Moser", + oepsNummer = "O-44332", + feiId = "10011111", + verein = "URFV Neumarkt/M.", + lizenzKlasse = "R2D2", + startkartAktiv = true, + startkartSaison = 2026, + nation = "AUT", + vereinsNummer = "4-001" + ), + ) + + val funktionaere: SnapshotStateList = mutableStateListOf( + Funktionaer( + id = 1, + vorname = "Wolfgang", + nachname = "Schier", + richterNummer = "100123", + rollen = listOf("RICHTER"), + richterQualifikation = "G3", + qualifiziertFuerSparten = listOf("DRESSUR", "SPRINGEN"), + email = "wolfgang.schier@oeps.at", + vereinsNummer = "4-001" + ), + Funktionaer( + id = 2, + vorname = "Alice", + nachname = "Schwab", + richterNummer = "100456", + rollen = listOf("RICHTER", "TBA"), + richterQualifikation = "INTERNATIONAL", + qualifiziertFuerSparten = listOf("DRESSUR"), + email = "alice.schwab@oeps.at", + vereinsNummer = "4-002" + ), + Funktionaer( + id = 3, + vorname = "Dietmar", + nachname = "Gstöttner", + richterNummer = "100789", + rollen = listOf("PARCOURSBAUER"), + email = "dietmar.gstoettner@oeps.at", + vereinsNummer = "4-003" + ), + ) + val oepsStammdaten: List = listOf( - Verein(1001, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."), - Verein(1002, "Pferdesportverein Linz", "V-OOE-0002", "Linz"), - Verein(1003, "Reitclub Ebelsberg", "V-OOE-0003", "Linz-Ebelsberg"), - Verein(1004, "Union Reitverein Gschwandt", "V-OOE-0004", "Gschwandt"), - Verein(1005, "Reitsportclub Gleisdorf", "V-ST-0005", "Gleisdorf"), - Verein(1006, "Pferdesportzentrum Stadl-Paura", "V-OOE-0006", "Stadl-Paura"), + Verein( + 1001, + "Union Reit- und Fahrverein Neumarkt/M.", + "4-001", + ort = "Neumarkt/M.", + bundesland = "OÖ", + istVeranstalter = true + ), + Verein(1002, "Pferdesportverein Linz", "4-002", ort = "Linz", bundesland = "OÖ", istVeranstalter = true), + Verein(1003, "Reitclub Ebelsberg", "4-003", ort = "Linz-Ebelsberg", bundesland = "OÖ", istVeranstalter = true), + Verein(1004, "Union Reitverein Gschwandt", "4-004", ort = "Gschwandt", bundesland = "OÖ", istVeranstalter = true), + Verein(1005, "Reitsportclub Gleisdorf", "5-005", ort = "Gleisdorf", bundesland = "ST", istVeranstalter = true), + Verein( + 1006, + "Pferdesportzentrum Stadl-Paura", + "4-006", + ort = "Stadl-Paura", + bundesland = "OÖ", + istVeranstalter = true + ), ) val vereine: SnapshotStateList = mutableStateListOf( - Verein(1, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."), - Verein(2, "Pferdesportverein Linz", "V-OOE-0002", "Linz"), + Verein( + 1, + "Union Reit- und Fahrverein Neumarkt/M.", + "4-001", + ort = "Neumarkt/M.", + bundesland = "OÖ", + istVeranstalter = true + ), + Verein(2, "Pferdesportverein Linz", "4-002", ort = "Linz", bundesland = "OÖ", istVeranstalter = true), ) fun addVerein(name: String, oeps: String, ort: String): Long { val id = (vereine.maxOfOrNull { it.id } ?: 0) + 1 - vereine.add(Verein(id, name, oeps, ort)) + vereine.add(Verein(id, name, oeps, ort = ort)) return id } @@ -96,7 +313,7 @@ object StoreV2 { datumVon = "2026-05-20", datumBis = "2026-05-24", status = "In Vorbereitung", - beschreibung = "Großes Reit-Event am Ebelsberger Schlosspark." + beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark." ) ) TurnierStoreV2.add( 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/v2/VeranstaltungScreens.kt index 3a405019..56a293b9 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/v2/VeranstaltungScreens.kt @@ -28,37 +28,81 @@ import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @Composable -fun VeranstaltungenUebersichtV2( - onEventOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId - onNewEvent: () -> Unit +fun VeranstaltungVerwaltungV2( + onVeranstaltungOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId + onNewVeranstaltung: () -> Unit, + onNavigateToPferde: () -> Unit, + onNavigateToReiter: () -> Unit, + onNavigateToVereine: () -> Unit, + onNavigateToFunktionaere: () -> Unit, + onNavigateToVeranstalter: () -> Unit, + onNavigateToZnsImport: () -> Unit ) { DesktopThemeV2 { - val allEvents = remember { StoreV2.allEvents() } + val allVeranstaltungen = remember { StoreV2.allEvents() } val vereine = StoreV2.vereine var searchQuery by remember { mutableStateOf("") } var selectedStatus by remember { mutableStateOf(null) } - val availableStatuses = remember(allEvents) { allEvents.map { it.status }.distinct().sorted() } + val availableStatuses = remember(allVeranstaltungen) { allVeranstaltungen.map { it.status }.distinct().sorted() } - val filteredEvents = remember(allEvents, searchQuery, selectedStatus) { - allEvents.filter { event -> - val verein = vereine.find { it.id == event.veranstalterId } - val matchesSearch = event.titel.contains(searchQuery, ignoreCase = true) || + val filteredVeranstaltungen = remember(allVeranstaltungen, searchQuery, selectedStatus) { + allVeranstaltungen.filter { veranstaltung -> + val verein = vereine.find { it.id == veranstaltung.veranstalterId } + val matchesSearch = veranstaltung.titel.contains(searchQuery, ignoreCase = true) || (verein?.name?.contains(searchQuery, ignoreCase = true) ?: false) - val matchesStatus = selectedStatus == null || event.status == selectedStatus + val matchesStatus = selectedStatus == null || veranstaltung.status == selectedStatus matchesSearch && matchesStatus }.sortedByDescending { it.datumVon } } Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + // Navigation Toolbar (Top) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AssistChip( + onClick = onNavigateToPferde, + label = { Text("Pferde") }, + leadingIcon = { Icon(Icons.Default.Pets, null) }) + AssistChip( + onClick = onNavigateToReiter, + label = { Text("Reiter") }, + leadingIcon = { Icon(Icons.Default.Person, null) }) + AssistChip( + onClick = onNavigateToVereine, + label = { Text("Vereine") }, + leadingIcon = { Icon(Icons.Default.Home, null) }) + AssistChip( + onClick = onNavigateToFunktionaere, + label = { Text("Funktionäre") }, + leadingIcon = { Icon(Icons.Default.Badge, null) }) + AssistChip( + onClick = onNavigateToVeranstalter, + label = { Text("Veranstalter") }, + leadingIcon = { Icon(Icons.Default.Business, null) }) + VerticalDivider(Modifier.height(32.dp).padding(horizontal = 4.dp)) + AssistChip( + onClick = onNavigateToZnsImport, + label = { Text("ZNS Importer") }, + leadingIcon = { Icon(Icons.Default.CloudDownload, null) }, + colors = AssistChipDefaults.assistChipColors( + labelColor = MaterialTheme.colorScheme.primary, + leadingIconContentColor = MaterialTheme.colorScheme.primary + ) + ) + } + // Header Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text("Alle Veranstaltungen", style = MaterialTheme.typography.headlineMedium) - Button(onClick = onNewEvent) { + Text("Veranstaltung-Verwaltung", style = MaterialTheme.typography.headlineMedium) + Button(onClick = onNewVeranstaltung) { Icon(Icons.Default.Add, contentDescription = null) Spacer(Modifier.width(8.dp)) Text("Neue Veranstaltung") @@ -109,7 +153,7 @@ fun VeranstaltungenUebersichtV2( } } - if (filteredEvents.isEmpty()) { + if (filteredVeranstaltungen.isEmpty()) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( if (searchQuery.isEmpty() && selectedStatus == null) "Keine Veranstaltungen gefunden." @@ -119,23 +163,24 @@ fun VeranstaltungenUebersichtV2( } } else { LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) { - items(filteredEvents) { event -> - val verein = vereine.find { it.id == event.veranstalterId } + items(filteredVeranstaltungen) { veranstaltung -> + val verein = vereine.find { it.id == veranstaltung.veranstalterId } Card( - modifier = Modifier.fillMaxWidth().clickable { onEventOpen(event.veranstalterId, event.id) }, + modifier = Modifier.fillMaxWidth() + .clickable { onVeranstaltungOpen(veranstaltung.veranstalterId, veranstaltung.id) }, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Column(Modifier.weight(1f)) { - Text(event.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text(veranstaltung.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text( - "${verein?.name ?: "Unbekannter Verein"} | ${event.datumVon} bis ${event.datumBis ?: ""}", + "${verein?.name ?: "Unbekannter Verein"} | ${veranstaltung.datumVon} bis ${veranstaltung.datumBis ?: ""}", style = MaterialTheme.typography.bodySmall ) - if (event.beschreibung.isNotEmpty()) { + if (veranstaltung.beschreibung.isNotEmpty()) { Spacer(Modifier.height(4.dp)) Text( - event.beschreibung, + veranstaltung.beschreibung, style = MaterialTheme.typography.bodyMedium, maxLines = 2, color = Color.DarkGray @@ -147,7 +192,7 @@ fun VeranstaltungenUebersichtV2( shape = MaterialTheme.shapes.small ) { Text( - event.status, + veranstaltung.status, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onPrimaryContainer @@ -213,7 +258,7 @@ fun VeranstalterAnlegenWizard( if (searchQuery.length < 2) emptyList() else StoreV2.oepsStammdaten.filter { it.name.contains(searchQuery, ignoreCase = true) || - it.ort.contains(searchQuery, ignoreCase = true) || + (it.ort?.contains(searchQuery, ignoreCase = true) ?: false) || it.oepsNummer.contains(searchQuery, ignoreCase = true) } } @@ -223,11 +268,11 @@ fun VeranstalterAnlegenWizard( items(results) { v -> ListItem( headlineContent = { Text(v.name) }, - supportingContent = { Text("${v.ort} | ${v.oepsNummer}") }, + supportingContent = { Text("${v.ort ?: ""} | ${v.oepsNummer}") }, modifier = Modifier.clickable { name = v.name oeps = v.oepsNummer - ort = v.ort + ort = v.ort ?: "" step = 2 } ) @@ -409,7 +454,7 @@ fun VeranstaltungKonfigV2( var search by remember { mutableStateOf("") } val filteredVereine = remember(search) { StoreV2.vereine.filter { - it.name.contains(search, ignoreCase = true) || it.ort.contains(search, ignoreCase = true) + it.name.contains(search, ignoreCase = true) || (it.ort?.contains(search, ignoreCase = true) ?: false) } } @@ -444,7 +489,7 @@ fun VeranstaltungKonfigV2( ) { Column(Modifier.weight(1f)) { Text(verein.name, fontWeight = FontWeight.Bold) - Text("${verein.ort} | ${verein.oepsNummer}", style = MaterialTheme.typography.bodySmall) + Text("${verein.ort ?: ""} | ${verein.oepsNummer}", style = MaterialTheme.typography.bodySmall) } if (isSelected) Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null) } @@ -700,12 +745,13 @@ object TurnierStoreV2 { } @Composable -fun VeranstaltungUebersichtV2( +fun VeranstaltungProfilScreen( veranstalterId: Long, veranstaltungId: Long, onBack: () -> Unit, onTurnierNeu: () -> Unit, onTurnierOpen: (Long) -> Unit, + onNavigateToVeranstalterProfil: (Long) -> Unit, ) { DesktopThemeV2 { val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } @@ -732,6 +778,15 @@ fun VeranstaltungUebersichtV2( } } Spacer(Modifier.weight(1f)) + + AssistChip( + onClick = { onNavigateToVeranstalterProfil(veranstalterId) }, + label = { Text("Veranstalter-Profil") }, + leadingIcon = { Icon(Icons.Default.Business, contentDescription = null, modifier = Modifier.size(18.dp)) } + ) + + Spacer(Modifier.width(8.dp)) + ElevatedButton( onClick = onTurnierNeu, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) @@ -1244,7 +1299,7 @@ private fun Step2Sparten( isError = !isDateValid && tVon != null && vVon != null && tVon.isBefore(vVon), supportingText = { if (!isDateValid && tVon != null && vVon != null && tVon.isBefore(vVon)) { - Text("Muss innerhalb der Veranstaltung liegen (${veranstaltung?.datumVon})") + Text("Muss innerhalb der Veranstaltung liegen (${veranstaltung.datumVon})") } } ) @@ -1264,7 +1319,7 @@ private fun Step2Sparten( ))), supportingText = { if (!isDateValid && tBis != null) { - if (vBis != null && tBis.isAfter(vBis)) Text("Darf nicht nach der Veranstaltung enden (${veranstaltung?.datumBis})") + if (vBis != null && tBis.isAfter(vBis)) Text("Darf nicht nach der Veranstaltung enden (${veranstaltung.datumBis})") else if (tVon != null && tBis.isBefore(tVon)) Text("Darf nicht vor dem Startdatum liegen") } }