Compare commits
2 Commits
09debdef86
...
b990f4dc05
| Author | SHA1 | Date | |
|---|---|---|---|
| b990f4dc05 | |||
| 6fc6c8fc79 |
|
|
@ -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.*
|
||||
|
||||
|
|
|
|||
75
docs/06_Frontend/screen-flow_1-04-26.md
Normal file
75
docs/06_Frontend/screen-flow_1-04-26.md
Normal file
|
|
@ -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
|
||||
```
|
||||
BIN
docs/06_Frontend/uebersicht-v01.png
Normal file
BIN
docs/06_Frontend/uebersicht-v01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 868 KiB |
BIN
docs/06_Frontend/uebersicht-v02.png
Normal file
BIN
docs/06_Frontend/uebersicht-v02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/06_Frontend/verwaltung_01-04-26.png
Normal file
BIN
docs/06_Frontend/verwaltung_01-04-26.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
|
|
@ -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.*
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Session Log - 01. April 2026 - Back-Stack Navigation
|
||||
|
||||
## Ziel
|
||||
|
||||
Implementierung einer intelligenten "Zurück"-Navigation, die sich den Verlauf der besuchten Screens merkt.
|
||||
|
||||
## Änderungen
|
||||
|
||||
- **Core Navigation**: `NavigationPort` um `navigateBack()` Methode erweitert.
|
||||
- **Desktop Navigation**: `DesktopNavigationPort` mit einem internen `backStack` (MutableList) ausgestattet, um den
|
||||
Verlauf zu speichern.
|
||||
- **UI Layout**: `DesktopMainLayout` und `DesktopTopBar` auf `onBack` umgestellt.
|
||||
- **Screen Integration**: Alle Screens im `DesktopContentArea` nutzen nun den globalen `onBack` Callback, statt fest
|
||||
codierte Ziel-Screens für die Rücknavigation zu verwenden.
|
||||
|
||||
## Ergebnis
|
||||
|
||||
- Ein Klick auf den "Zurück"-Pfeil in der TopBar führt nun immer zum unmittelbar vorherigen Screen.
|
||||
- Beispiel: Veranstaltung-Profil -> Veranstalter-Profil -> Zurück -> Veranstaltung-Profil (funktioniert jetzt korrekt).
|
||||
|
||||
## Status
|
||||
|
||||
- **Abgeschlossen** (Alle Screens im V2-Flow unterstützt).
|
||||
41
docs/99_Journal/2026-04-01_Session_Log_Zentrale_ZNS.md
Normal file
41
docs/99_Journal/2026-04-01_Session_Log_Zentrale_ZNS.md
Normal file
|
|
@ -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*
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,35 @@ class DesktopNavigationPort : NavigationPort {
|
|||
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.Onboarding)
|
||||
override val currentScreen: StateFlow<AppScreen> = _currentScreen.asStateFlow()
|
||||
|
||||
// Backstack zur Speicherung des Verlaufs
|
||||
private val backStack = mutableListOf<AppScreen>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <T> ManagementTableScreen(
|
||||
title: String,
|
||||
items: List<T>,
|
||||
columns: List<TableColumn<T>>,
|
||||
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<T>(
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<String> = emptyList(),
|
||||
var richterQualifikation: String? = null,
|
||||
var qualifiziertFuerSparten: List<String> = 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<Pferd> = 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<Reiter> = 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<Funktionaer> = 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<Verein> = 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<Verein> = 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(
|
||||
|
|
|
|||
|
|
@ -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<String?>(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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user