feat(event-feature): enhance Veranstaltungs- & Turnier-Workflow
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
- Extended `Veranstaltung` domain model with new fields: `untertitel`, `logoUrl`, and `sponsoren`. - Refined navigation in `DesktopMainLayout.kt` to check turnier context and improve routing. - Overhauled `TurnierStammdatenTab` with enhanced interactivity: dynamic chip-based selectors for Spartens, Klassen, and Sponsors, as well as date pickers and ZNS import handling. - Implemented validations for date ranges and required fields. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+3
@@ -40,7 +40,10 @@ data class Veranstaltung(
|
|||||||
|
|
||||||
// Basic Information
|
// Basic Information
|
||||||
var name: String,
|
var name: String,
|
||||||
|
var untertitel: String? = null,
|
||||||
var beschreibung: String? = null,
|
var beschreibung: String? = null,
|
||||||
|
var logoUrl: String? = null,
|
||||||
|
var sponsoren: String? = null, // JSON string or comma-separated for now
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||||
|
|||||||
+6
@@ -153,7 +153,10 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
|||||||
return Veranstaltung(
|
return Veranstaltung(
|
||||||
veranstaltungId = row[VeranstaltungTable.id].value,
|
veranstaltungId = row[VeranstaltungTable.id].value,
|
||||||
name = row[VeranstaltungTable.name],
|
name = row[VeranstaltungTable.name],
|
||||||
|
untertitel = row[VeranstaltungTable.untertitel],
|
||||||
beschreibung = row[VeranstaltungTable.beschreibung],
|
beschreibung = row[VeranstaltungTable.beschreibung],
|
||||||
|
logoUrl = row[VeranstaltungTable.logoUrl],
|
||||||
|
sponsoren = row[VeranstaltungTable.sponsoren],
|
||||||
startDatum = row[VeranstaltungTable.startDatum],
|
startDatum = row[VeranstaltungTable.startDatum],
|
||||||
endDatum = row[VeranstaltungTable.endDatum],
|
endDatum = row[VeranstaltungTable.endDatum],
|
||||||
ort = row[VeranstaltungTable.ort],
|
ort = row[VeranstaltungTable.ort],
|
||||||
@@ -173,7 +176,10 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
|||||||
*/
|
*/
|
||||||
private fun veranstaltungToStatement(statement: UpdateBuilder<*>, veranstaltung: Veranstaltung) {
|
private fun veranstaltungToStatement(statement: UpdateBuilder<*>, veranstaltung: Veranstaltung) {
|
||||||
statement[VeranstaltungTable.name] = veranstaltung.name
|
statement[VeranstaltungTable.name] = veranstaltung.name
|
||||||
|
statement[VeranstaltungTable.untertitel] = veranstaltung.untertitel
|
||||||
statement[VeranstaltungTable.beschreibung] = veranstaltung.beschreibung
|
statement[VeranstaltungTable.beschreibung] = veranstaltung.beschreibung
|
||||||
|
statement[VeranstaltungTable.logoUrl] = veranstaltung.logoUrl
|
||||||
|
statement[VeranstaltungTable.sponsoren] = veranstaltung.sponsoren
|
||||||
statement[VeranstaltungTable.startDatum] = veranstaltung.startDatum
|
statement[VeranstaltungTable.startDatum] = veranstaltung.startDatum
|
||||||
statement[VeranstaltungTable.endDatum] = veranstaltung.endDatum
|
statement[VeranstaltungTable.endDatum] = veranstaltung.endDatum
|
||||||
statement[VeranstaltungTable.ort] = veranstaltung.ort
|
statement[VeranstaltungTable.ort] = veranstaltung.ort
|
||||||
|
|||||||
+3
@@ -16,7 +16,10 @@ object VeranstaltungTable : UUIDTable("veranstaltungen") {
|
|||||||
|
|
||||||
// Basic Information
|
// Basic Information
|
||||||
val name = varchar("name", 255)
|
val name = varchar("name", 255)
|
||||||
|
val untertitel = varchar("untertitel", 255).nullable()
|
||||||
val beschreibung = text("beschreibung").nullable()
|
val beschreibung = text("beschreibung").nullable()
|
||||||
|
val logoUrl = varchar("logo_url", 255).nullable()
|
||||||
|
val sponsoren = text("sponsoren").nullable() // JSON array of Sponsor data
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
val startDatum = date("start_datum")
|
val startDatum = date("start_datum")
|
||||||
|
|||||||
+1
@@ -67,6 +67,7 @@ data class DomVerein(
|
|||||||
|
|
||||||
// Status & Verwaltung
|
// Status & Verwaltung
|
||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
|
var logoUrl: String? = null,
|
||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||||
|
|
||||||
|
|||||||
+4
@@ -34,6 +34,7 @@ class ExposedVereinRepository : VereinRepository {
|
|||||||
oepsRegionNummer = row[VereinTable.oepsRegionNummer],
|
oepsRegionNummer = row[VereinTable.oepsRegionNummer],
|
||||||
istVeranstalter = row[VereinTable.istVeranstalter],
|
istVeranstalter = row[VereinTable.istVeranstalter],
|
||||||
istAktiv = row[VereinTable.istAktiv],
|
istAktiv = row[VereinTable.istAktiv],
|
||||||
|
logoUrl = row[VereinTable.logoUrl],
|
||||||
bemerkungen = row[VereinTable.bemerkungen],
|
bemerkungen = row[VereinTable.bemerkungen],
|
||||||
datenQuelle = DatenQuelleE.valueOf(row[VereinTable.datenQuelle]),
|
datenQuelle = DatenQuelleE.valueOf(row[VereinTable.datenQuelle]),
|
||||||
createdAt = row[VereinTable.createdAt],
|
createdAt = row[VereinTable.createdAt],
|
||||||
@@ -106,6 +107,7 @@ class ExposedVereinRepository : VereinRepository {
|
|||||||
it[oepsRegionNummer] = verein.oepsRegionNummer
|
it[oepsRegionNummer] = verein.oepsRegionNummer
|
||||||
it[istVeranstalter] = verein.istVeranstalter
|
it[istVeranstalter] = verein.istVeranstalter
|
||||||
it[istAktiv] = verein.istAktiv
|
it[istAktiv] = verein.istAktiv
|
||||||
|
it[logoUrl] = verein.logoUrl
|
||||||
it[bemerkungen] = verein.bemerkungen
|
it[bemerkungen] = verein.bemerkungen
|
||||||
it[datenQuelle] = verein.datenQuelle.name
|
it[datenQuelle] = verein.datenQuelle.name
|
||||||
it[updatedAt] = verein.updatedAt
|
it[updatedAt] = verein.updatedAt
|
||||||
@@ -127,6 +129,7 @@ class ExposedVereinRepository : VereinRepository {
|
|||||||
it[oepsRegionNummer] = verein.oepsRegionNummer
|
it[oepsRegionNummer] = verein.oepsRegionNummer
|
||||||
it[istVeranstalter] = verein.istVeranstalter
|
it[istVeranstalter] = verein.istVeranstalter
|
||||||
it[istAktiv] = verein.istAktiv
|
it[istAktiv] = verein.istAktiv
|
||||||
|
it[logoUrl] = verein.logoUrl
|
||||||
it[bemerkungen] = verein.bemerkungen
|
it[bemerkungen] = verein.bemerkungen
|
||||||
it[datenQuelle] = verein.datenQuelle.name
|
it[datenQuelle] = verein.datenQuelle.name
|
||||||
it[createdAt] = verein.createdAt
|
it[createdAt] = verein.createdAt
|
||||||
@@ -169,6 +172,7 @@ class ExposedVereinRepository : VereinRepository {
|
|||||||
it[oepsRegionNummer] = toUpdate.oepsRegionNummer
|
it[oepsRegionNummer] = toUpdate.oepsRegionNummer
|
||||||
it[istVeranstalter] = toUpdate.istVeranstalter
|
it[istVeranstalter] = toUpdate.istVeranstalter
|
||||||
it[istAktiv] = toUpdate.istAktiv
|
it[istAktiv] = toUpdate.istAktiv
|
||||||
|
it[logoUrl] = toUpdate.logoUrl
|
||||||
it[bemerkungen] = toUpdate.bemerkungen
|
it[bemerkungen] = toUpdate.bemerkungen
|
||||||
it[datenQuelle] = toUpdate.datenQuelle.name
|
it[datenQuelle] = toUpdate.datenQuelle.name
|
||||||
it[updatedAt] = toUpdate.updatedAt
|
it[updatedAt] = toUpdate.updatedAt
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
|
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||||
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verknüpfungstabelle zwischen Verein und Reiter (Ansprechpersonen/Funktionäre).
|
||||||
|
*/
|
||||||
|
object VereinAnsprechpersonTable : Table("verein_ansprechperson") {
|
||||||
|
val vereinId = uuid("verein_id").references(VereinTable.id)
|
||||||
|
val reiterId = uuid("reiter_id").references(ReiterTable.id)
|
||||||
|
val rolle = varchar("rolle", 100).nullable() // z.B. "Obmann", "Meldestelle", "Sportwart"
|
||||||
|
val istHauptkontakt = bool("ist_hauptkontakt").default(false)
|
||||||
|
|
||||||
|
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||||
|
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(vereinId, reiterId)
|
||||||
|
}
|
||||||
+1
@@ -24,6 +24,7 @@ object VereinTable : Table("verein") {
|
|||||||
val oepsRegionNummer = varchar("oeps_region_nummer", 10).nullable()
|
val oepsRegionNummer = varchar("oeps_region_nummer", 10).nullable()
|
||||||
val istVeranstalter = bool("ist_veranstalter").default(false)
|
val istVeranstalter = bool("ist_veranstalter").default(false)
|
||||||
val istAktiv = bool("ist_aktiv").default(true)
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
|
val logoUrl = varchar("logo_url", 255).nullable()
|
||||||
val bemerkungen = text("bemerkungen").nullable()
|
val bemerkungen = text("bemerkungen").nullable()
|
||||||
val datenQuelle = varchar("daten_quelle", 50)
|
val datenQuelle = varchar("daten_quelle", 50)
|
||||||
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||||
|
|||||||
@@ -1,3 +1,108 @@
|
|||||||
|
## Nachtrag 31.03.2026 17:35
|
||||||
|
|
||||||
|
- **Validierung & Konsistenz im Turnier-Workflow:**
|
||||||
|
- **Veranstaltung anlegen:** In `VeranstaltungKonfigV2` wurde eine Sperre für Vergangenheits-Daten implementiert. Das
|
||||||
|
Startdatum darf nicht vor dem aktuellen Tag liegen. Entsprechende UI-Fehlermeldungen und eine Button-Deaktivierung
|
||||||
|
wurden hinzugefügt.
|
||||||
|
- **Turnier-Stammdaten (Bearbeiten):** Der Tab "STAMMDATEN" im `TurnierDetailScreen` wurde vollständig überarbeitet
|
||||||
|
und spiegelt nun die Logik des `TurnierWizardV2` (Option 1) wider.
|
||||||
|
- **Validierung:** 5-stellige Turnier-Nr. muss explizit bestätigt werden.
|
||||||
|
- **ZNS-Import:** Statusanzeige (geladen/nicht geladen) und interaktive Import-Buttons (Internet/USB) wurden
|
||||||
|
integriert.
|
||||||
|
- **Regelwerk:** Dynamische Generierung von Kategorien (inkl. Pony-Kategorien) basierend auf Sparten- und
|
||||||
|
Klassenauswahl via Filter-Chips.
|
||||||
|
- **Datum & Ort:** Integration von Material 3 DatePickern. Hinweise auf die erforderliche Übereinstimmung mit dem
|
||||||
|
Veranstaltungszeitraum und -ort wurden hinzugefügt.
|
||||||
|
- **Branding:** Unterstützung für Titel, Sub-Titel und dynamische Sponsoren-Chips direkt im Stammdaten-Tab.
|
||||||
|
- **UI/UX:** Einsatz von `FlowRow`, `InputChip` und `SectionCard` für ein aufgeräumtes, konsistentes Erscheinungsbild
|
||||||
|
über alle Turnier-Screens hinweg.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 17:15
|
||||||
|
|
||||||
|
- **`TurnierWizardV2` - Regelwerks-Kategorien & Pony-Logik:**
|
||||||
|
- **Refactoring Kategorien:** Turniere unterstützen nun mehrere gleichberechtigte Kategorien (z.B. "CDN-C NEU" und "
|
||||||
|
CDNP-C NEU") gleichzeitig. Dies ist entscheidend für die korrekte Anwendung der Regelwerke (z.B. Nationales
|
||||||
|
Dressur-Turnier vs. Nationales Dressur-Turnier Pony).
|
||||||
|
- **Integration Pony-Status:** Der separate Switch für "Pony-Bewerbe" wurde entfernt. Stattdessen werden
|
||||||
|
Pony-Kategorien (Suffix "P") nun direkt als auswählbare Optionen in den Kategorien-Vorschlägen angeboten, sofern
|
||||||
|
eine Sparte und Klasse gewählt wurde.
|
||||||
|
- **Datenmodell `TurnierV2`:** Das Feld `isPony` wurde entfernt, da der Status nun implizit über die gewählten
|
||||||
|
Kategorien definiert ist.
|
||||||
|
- **Automatisierung:** Bei Eingabe der Turnier-Nummern für Neumarkt (26128, 26129) werden nun automatisch sowohl die
|
||||||
|
Standard- als auch die Pony-Kategorie vorselektiert.
|
||||||
|
- **Seed-Daten:** Die Testdaten in `Stores.kt` wurden aktualisiert, um Turniere mit mehreren Kategorien (CDN + CDNP)
|
||||||
|
abzubilden.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 17:10
|
||||||
|
|
||||||
|
- **`TurnierWizardV2` - Klassen & Pony-Bewerbe:**
|
||||||
|
- **Klassen-Auswahl:** Umstellung auf ein modernes Chip-basiertes Layout (Grid). Die Klassen (C-NEU bis S) werden nun
|
||||||
|
als `FilterChip` dargestellt, was die Mehrfachauswahl intuitiver macht.
|
||||||
|
- **Pony-Bewerbe:** Integration einer neuen "Pony-Bewerbe" Option (Switch) in Schritt 2. Diese Option steuert die
|
||||||
|
sportfachliche Kennzeichnung des Turniers.
|
||||||
|
- **Kategorien-Logik (CDNP/CSNP):** Die automatische Generierung der Kategorien-Vorschläge berücksichtigt nun den
|
||||||
|
Pony-Status. Bei aktiviertem Switch wird automatisch das Suffix "P" (z.B. CDNP statt CDN) verwendet.
|
||||||
|
- **UI/UX Refinement:**
|
||||||
|
- Einsatz von `InputChip` für die Kategorien-Auswahl mit Checkmark-Indikator.
|
||||||
|
- Gruppierung der Optionen (Sparten, Pony, Klassen) in einer übersichtlichen Spalten/Zeilen-Struktur mit
|
||||||
|
verbesserten Abständen.
|
||||||
|
- Manuelle Korrekturmöglichkeit der Kategorie im `OutlinedTextField` mit `leadingIcon`.
|
||||||
|
- **Datenmodell & Seed:** Erweiterung von `TurnierV2` um das Feld `isPony` und Aktualisierung der Seed-Daten für "
|
||||||
|
Neumarkt 2026" auf den neuen Pony-Status.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 16:45
|
||||||
|
|
||||||
|
- **`TurnierWizardV2` - ZNS-Import & Regelwerks-Logik:**
|
||||||
|
- **Schritt 1 (Basics):** Überarbeitung der Turnier-Nr. Erfassung mit explizitem Bestätigungs-Button und Validierung (
|
||||||
|
5 Stellen).
|
||||||
|
- **ZNS-Import:** Implementierung von zwei Import-Wegen (Internet / USB). Ein interaktiver Fortschritts-Dialog
|
||||||
|
simuliert die Datenverarbeitung und setzt den `znsDataLoaded`-Status.
|
||||||
|
- **ZNS-Statusanzeige:** Ein markantes Status-Panel (Grün/Rot) zeigt an, ob die Pflicht-Stammdaten geladen wurden.
|
||||||
|
Der "Weiter"-Button ist erst nach erfolgreichem Import aktiv.
|
||||||
|
- **Schritt 2 (Sparten & Klassen):** Erweiterung der Klassen-Auswahl (C-NEU bis S) in einem übersichtlichen
|
||||||
|
Spalten-Layout.
|
||||||
|
- **Intelligente Kategorien-Vorschläge:** Implementierung einer Logik, die basierend auf den gewählten Sparten und
|
||||||
|
Klassen passende Turnier-Kategorien (z.B. CSN-C-NEU, CDN-A) als Filter-Chips vorschlägt.
|
||||||
|
- **Modell-Update:** `TurnierV2` um `znsDataLoaded` erweitert und die Sprach-Auswahl gemäß Benutzerwunsch entfernt.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 16:30
|
||||||
|
|
||||||
|
- **`TurnierWizardV2` - "Meta"-Daten & Stammdaten:**
|
||||||
|
- Der Wizard zur Neuanlage eines Turniers wurde gemäß den Benutzervorgaben (Screenshots `Turnier-Stammdaten_01/02`)
|
||||||
|
umfassend erweitert und in drei Phasen unterteilt.
|
||||||
|
- **Schritt 1: Basiskonfiguration:** Erfassung der 5-stelligen Turnier-Nr., des Typs (ÖTO National / FEI
|
||||||
|
International), Sprache (Deutsch/Englisch) und Integration von Platzhalter-Buttons für den ZNS-Daten-Import (
|
||||||
|
Internet/USB) inkl. Informations-Dialog.
|
||||||
|
- **Schritt 2: Sparten & Klassen:** Auswahl der Disziplinen (Dressur, Springen) und Klassen (C, B, A). Die Kategorien
|
||||||
|
werden basierend auf der Auswahl freigeschaltet und bei bekannten Nummern (z.B. 26128) automatisch vorbelegt.
|
||||||
|
- **Schritt 3: Branding & Sponsoren:** Erfassung von Turnier-Titel, Sub-Titel und einer dynamisch erweiterbaren
|
||||||
|
Sponsorenliste (analog zum Veranstaltungs-Wizard).
|
||||||
|
- **Datenmodell `TurnierV2`:** Erweiterung um alle neuen Felder (`typ`, `sprache`, `sparten`, `klassen`, `titel`,
|
||||||
|
`subTitel`, `sponsoren`) unter Nutzung von `SnapshotStateList` für reaktive UI-Updates.
|
||||||
|
- **UI/UX Polish:** Nutzung von `LinearProgressIndicator`, `RadioButton`-Gruppen, `Checkbox`-Listen und
|
||||||
|
`verticalScroll` für eine flüssige Bedienung auf kleineren Bildschirmen.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 16:15
|
||||||
|
|
||||||
|
- **Event-Cockpit-Optimierung:**
|
||||||
|
- **`VeranstaltungUebersichtV2`:** Umfassendes UI-Update für das Veranstaltungs-Cockpit.
|
||||||
|
- **KPI-Dashboard:** Integration von vier KPI-Karten (Turniere, Nennungen, Reiter, Pferde) für eine schnelle Übersicht
|
||||||
|
des Event-Status.
|
||||||
|
- **Turnier-Liste:** Umstellung auf ein modernes Card-Layout mit `OutlinedCard`, `SuggestionChip` für Kategorien und
|
||||||
|
verbesserten Action-Buttons (Öffnen/Löschen).
|
||||||
|
- **Turnier-Wizard:** Die Validierung der 5-stelligen Turnier-Nummer wurde durch `supportingText` im Textfeld
|
||||||
|
verbessert.
|
||||||
|
|
||||||
|
- **Navigation & Routing:**
|
||||||
|
- **`DesktopMainLayout.kt`:** Die Navigation für `AppScreen.TurnierDetail` und `AppScreen.TurnierNeu` wurde
|
||||||
|
vollständig auf den `v2`-Store und die neuen Screens (`VeranstaltungUebersichtV2`, `TurnierWizardV2`) umgestellt.
|
||||||
|
- **Back-Navigation:** "Zurück"-Buttons in den Turnier-Screens führen nun logisch zurück zur
|
||||||
|
`VeranstaltungUebersichtV2` anstatt zu veralteten Screens.
|
||||||
|
|
||||||
|
- **Demonstrations-Daten:**
|
||||||
|
- Für das Beispiel-Event "Neumarkt 2026" (ID 100) wurden realistische Platzhalter-Werte in die KPIs integriert (z.B.
|
||||||
|
248 Nennungen), um das finale Look-and-Feel zu demonstrieren.
|
||||||
|
|
||||||
## Nachtrag 31.03.2026 15:45
|
## Nachtrag 31.03.2026 15:45
|
||||||
|
|
||||||
- **Fehlerbehebung Desktop-Shell Build:**
|
- **Fehlerbehebung Desktop-Shell Build:**
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,8 @@
|
|||||||
package at.mocode.turnier.feature.presentation
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -34,6 +36,11 @@ fun TurnierDetailScreen(
|
|||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
// Temporäre Lösung bis zur echten Repository-Anbindung:
|
||||||
|
// Da TurnierDetailScreen in einem anderen Modul liegt, übergeben wir
|
||||||
|
// die Veranstaltungsinformationen eigentlich via ViewModel.
|
||||||
|
// Hier nutzen wir vorerst koin oder Parameter.
|
||||||
|
|
||||||
val tabs = listOf(
|
val tabs = listOf(
|
||||||
"STAMMDATEN",
|
"STAMMDATEN",
|
||||||
"ORGANISATION",
|
"ORGANISATION",
|
||||||
|
|||||||
+262
-153
@@ -1,11 +1,11 @@
|
|||||||
package at.mocode.turnier.feature.presentation
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CloudDownload
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Usb
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||||
private val AccentBlue = Color(0xFF3B82F6)
|
private val AccentBlue = Color(0xFF3B82F6)
|
||||||
@@ -26,214 +27,328 @@ private val AccentBlue = Color(0xFF3B82F6)
|
|||||||
* - Turnier-Beschreibung: Titel, Sub-Titel
|
* - Turnier-Beschreibung: Titel, Sub-Titel
|
||||||
* - Sponsoren
|
* - Sponsoren
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StammdatenTabContent(turnierId: Long) {
|
fun StammdatenTabContent(turnierId: Long) {
|
||||||
|
// In einer echten App würden wir diese Daten aus einem ViewModel laden.
|
||||||
|
// Hier simulieren wir den State basierend auf den Anforderungen.
|
||||||
|
|
||||||
var turnierNr by remember { mutableStateOf("") }
|
var turnierNr by remember { mutableStateOf("") }
|
||||||
var typOto by remember { mutableStateOf(true) }
|
var nrConfirmed by remember { mutableStateOf(false) }
|
||||||
var spracheDe by remember { mutableStateOf(true) }
|
var znsDataLoaded by remember { mutableStateOf(false) }
|
||||||
var sparteDressur by remember { mutableStateOf(false) }
|
var typ by remember { mutableStateOf("ÖTO (National)") }
|
||||||
var sparteSpringen by remember { mutableStateOf(false) }
|
|
||||||
var klasseC by remember { mutableStateOf(false) }
|
val sparten = remember { mutableStateListOf<String>() }
|
||||||
var klasseB by remember { mutableStateOf(false) }
|
val klassen = remember { mutableStateListOf<String>() }
|
||||||
var klasseA by remember { mutableStateOf(false) }
|
val kat = remember { mutableStateListOf<String>() }
|
||||||
var datumVon by remember { mutableStateOf("") }
|
|
||||||
var datumBis by remember { mutableStateOf("") }
|
var von by remember { mutableStateOf("") }
|
||||||
|
var bis by remember { mutableStateOf("") }
|
||||||
|
var ort by remember { mutableStateOf("") }
|
||||||
|
|
||||||
var titel by remember { mutableStateOf("") }
|
var titel by remember { mutableStateOf("") }
|
||||||
var subTitel by remember { mutableStateOf("") }
|
var subTitel by remember { mutableStateOf("") }
|
||||||
|
val sponsoren = remember { mutableStateListOf<String>() }
|
||||||
|
|
||||||
|
var showZnsDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Hilfs-States für DatePicker
|
||||||
|
var showDatePickerVon by remember { mutableStateOf(false) }
|
||||||
|
var showDatePickerBis by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(scrollState)
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
) {
|
) {
|
||||||
// ── Turnier-Konfiguration ────────────────────────────────────────────
|
// ── Turnier-Konfiguration (Schritt 1 Logik) ───────────────────────────
|
||||||
SectionCard(title = "Turnier-Konfiguration") {
|
SectionCard(title = "Turnier-Konfiguration & ZNS") {
|
||||||
FormRow("Turnier-Nr.:") {
|
FormRow("Turnier-Nr.:") {
|
||||||
OutlinedTextField(
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
value = turnierNr,
|
OutlinedTextField(
|
||||||
onValueChange = { turnierNr = it },
|
value = turnierNr,
|
||||||
placeholder = { Text("z.B. 26128", fontSize = 13.sp) },
|
onValueChange = { if (it.length <= 5 && it.all { c -> c.isDigit() }) turnierNr = it },
|
||||||
modifier = Modifier.width(200.dp).height(48.dp),
|
placeholder = { Text("5-stellig", fontSize = 13.sp) },
|
||||||
singleLine = true,
|
modifier = Modifier.width(120.dp),
|
||||||
)
|
singleLine = true,
|
||||||
|
enabled = !nrConfirmed
|
||||||
|
)
|
||||||
|
if (!nrConfirmed) {
|
||||||
|
Button(
|
||||||
|
onClick = { nrConfirmed = true },
|
||||||
|
enabled = turnierNr.length == 5,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
|
||||||
|
) {
|
||||||
|
Text("Bestätigen")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InputChip(
|
||||||
|
selected = true,
|
||||||
|
onClick = { nrConfirmed = false },
|
||||||
|
label = { Text("Bestätigt") },
|
||||||
|
trailingIcon = { Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(16.dp)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (turnierNr.length == 5 && !nrConfirmed) {
|
||||||
|
Text(
|
||||||
|
"Bitte Turnier-Nummer bestätigen um fortzufahren.",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormRow("Typ:") {
|
FormRow("Typ:") {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
RadioButton(selected = typOto, onClick = { typOto = true })
|
FilterChip(
|
||||||
Text("OTO (National)", fontSize = 13.sp)
|
selected = typ == "ÖTO (National)",
|
||||||
RadioButton(selected = !typOto, onClick = { typOto = false })
|
onClick = { typ = "ÖTO (National)" },
|
||||||
Text("FEI (International)", fontSize = 13.sp)
|
label = { Text("ÖTO (National)") }
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = typ == "FEI (International)",
|
||||||
|
onClick = { typ = "FEI (International)" },
|
||||||
|
label = { Text("FEI (International)") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormRow("ZNS-Daten:") {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
FormRow("ZNS-Stammdaten:") {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {},
|
onClick = { showZnsDialog = true },
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue),
|
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(16.dp))
|
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Import via Internet", fontSize = 13.sp)
|
Text("Import via Internet")
|
||||||
}
|
}
|
||||||
OutlinedButton(onClick = {}) {
|
OutlinedButton(onClick = { showZnsDialog = true }) {
|
||||||
Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(16.dp))
|
Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Import via USB", fontSize = 13.sp)
|
Text("Import via USB")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
"Reiter-, Pferde-, Funktionärs- und Vereinsdaten vom OEPS Backend",
|
val znsStatusColor = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
fontSize = 11.sp,
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
color = Color(0xFF6B7280),
|
Icon(
|
||||||
)
|
if (znsDataLoaded) Icons.Default.CheckCircle else Icons.Default.Error,
|
||||||
}
|
contentDescription = null,
|
||||||
FormRow("Sprache:") {
|
tint = znsStatusColor,
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
modifier = Modifier.size(16.dp)
|
||||||
RadioButton(selected = spracheDe, onClick = { spracheDe = true })
|
)
|
||||||
Text("Deutsch", fontSize = 13.sp)
|
Text(
|
||||||
RadioButton(selected = !spracheDe, onClick = { spracheDe = false })
|
if (znsDataLoaded) "ZNS-Daten geladen" else "Keine ZNS-Daten geladen",
|
||||||
Text("English", fontSize = 13.sp)
|
color = znsStatusColor,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
}
|
||||||
FormRow("Sparten:") {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
// ── Sparten & Kategorien (Schritt 2 Logik) ───────────────────────────
|
||||||
Checkbox(checked = sparteDressur, onCheckedChange = { sparteDressur = it })
|
SectionCard(title = "Reglement & Sparten") {
|
||||||
Text("Dressur", fontSize = 13.sp)
|
FormRow("Sparte:") {
|
||||||
Checkbox(checked = sparteSpringen, onCheckedChange = { sparteSpringen = it })
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text("Springen", fontSize = 13.sp)
|
FilterChip(
|
||||||
|
selected = sparten.contains("Dressur"),
|
||||||
|
onClick = { if (sparten.contains("Dressur")) sparten.remove("Dressur") else sparten.add("Dressur") },
|
||||||
|
label = { Text("Dressur") }
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = sparten.contains("Springen"),
|
||||||
|
onClick = { if (sparten.contains("Springen")) sparten.remove("Springen") else sparten.add("Springen") },
|
||||||
|
label = { Text("Springen") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormRow("Klassen:") {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
FormRow("Klasse:") {
|
||||||
Checkbox(checked = klasseC, onCheckedChange = { klasseC = it })
|
val klassenListe = listOf("C-NEU", "C", "B", "A", "L", "LM", "M", "S")
|
||||||
Text("C", fontSize = 13.sp)
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Checkbox(checked = klasseB, onCheckedChange = { klasseB = it })
|
klassenListe.forEach { k ->
|
||||||
Text("B", fontSize = 13.sp)
|
FilterChip(
|
||||||
Checkbox(checked = klasseA, onCheckedChange = { klasseA = it })
|
selected = klassen.contains(k),
|
||||||
Text("A", fontSize = 13.sp)
|
onClick = { if (klassen.contains(k)) klassen.remove(k) else klassen.add(k) },
|
||||||
}
|
label = { Text(k) }
|
||||||
}
|
|
||||||
FormRow("Kategorien:") {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxWidth().height(60.dp),
|
|
||||||
color = Color(0xFFF3F4F6),
|
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
Text(
|
|
||||||
"Bitte Sparte(n) auswählen",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
color = Color(0xFF9CA3AF),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormRow("Datum:") {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
FormRow("Kategorien:") {
|
||||||
|
// Logik zur Generierung der Kategorien
|
||||||
|
val suggested = mutableListOf<String>()
|
||||||
|
sparten.forEach { s ->
|
||||||
|
val prefix = if (s == "Dressur") "CDN" else "CSN"
|
||||||
|
klassen.forEach { k ->
|
||||||
|
suggested.add("$prefix-$k")
|
||||||
|
suggested.add("${prefix}P-$k") // Pony Variante
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggested.isEmpty()) {
|
||||||
|
Text("Bitte Sparte und Klasse wählen", color = Color.Gray, fontSize = 13.sp)
|
||||||
|
} else {
|
||||||
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
suggested.forEach { c ->
|
||||||
|
InputChip(
|
||||||
|
selected = kat.contains(c),
|
||||||
|
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) },
|
||||||
|
label = { Text(c) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormRow("Zeitraum:") {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = datumVon,
|
value = von,
|
||||||
onValueChange = { datumVon = it },
|
onValueChange = {},
|
||||||
placeholder = { Text("DD.MM.YYYY", fontSize = 12.sp) },
|
label = { Text("Von") },
|
||||||
modifier = Modifier.width(160.dp).height(48.dp),
|
modifier = Modifier.width(160.dp).clickable { showDatePickerVon = true },
|
||||||
singleLine = true,
|
readOnly = true,
|
||||||
|
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
||||||
)
|
)
|
||||||
Text("bis", fontSize = 13.sp)
|
Text("bis")
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = datumBis,
|
value = bis,
|
||||||
onValueChange = { datumBis = it },
|
onValueChange = {},
|
||||||
placeholder = { Text("DD.MM.YYYY", fontSize = 12.sp) },
|
label = { Text("Bis") },
|
||||||
modifier = Modifier.width(160.dp).height(48.dp),
|
modifier = Modifier.width(160.dp).clickable { showDatePickerBis = true },
|
||||||
singleLine = true,
|
readOnly = true,
|
||||||
|
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Text("Hinweis: Muss innerhalb des Veranstaltungs-Zeitraums liegen.", fontSize = 11.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
FormRow("Ort:") {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = ort,
|
||||||
|
onValueChange = { ort = it },
|
||||||
|
label = { Text("Austragungsort") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
supportingText = { Text("Muss mit Veranstaltungsort übereinstimmen.") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Turnier-Beschreibung ─────────────────────────────────────────────
|
// ── Branding (Schritt 3 Logik) ───────────────────────────────────────
|
||||||
SectionCard(title = "Turnier-Beschreibung") {
|
SectionCard(title = "Turnier-Branding") {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = titel,
|
value = titel,
|
||||||
onValueChange = { titel = it },
|
onValueChange = { titel = it },
|
||||||
placeholder = { Text("z.B. Frühjahrs-Turnier 2026", fontSize = 13.sp) },
|
|
||||||
label = { Text("Titel") },
|
label = { Text("Titel") },
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth()
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = subTitel,
|
value = subTitel,
|
||||||
onValueChange = { subTitel = it },
|
onValueChange = { subTitel = it },
|
||||||
placeholder = { Text("z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ", fontSize = 13.sp) },
|
|
||||||
label = { Text("Sub-Titel") },
|
label = { Text("Sub-Titel") },
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth()
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// ── Sponsoren ────────────────────────────────────────────────────────
|
FormRow("Sponsoren:") {
|
||||||
SectionCard(
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
title = "Sponsoren",
|
sponsoren.forEach { s ->
|
||||||
action = {
|
InputChip(
|
||||||
TextButton(onClick = {}) {
|
selected = true,
|
||||||
Text("+ Sponsor hinzufügen", color = AccentBlue, fontSize = 13.sp)
|
onClick = { sponsoren.remove(s) },
|
||||||
}
|
label = { Text(s) },
|
||||||
},
|
trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(14.dp)) }
|
||||||
) {
|
)
|
||||||
Surface(
|
}
|
||||||
modifier = Modifier.fillMaxWidth().height(80.dp),
|
TextButton(onClick = { sponsoren.add("Neuer Sponsor") }) {
|
||||||
color = Color(0xFFF9FAFB),
|
Text("+ Hinzufügen")
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text("Noch keine Sponsoren hinzugefügt", fontSize = 13.sp, color = Color(0xFF6B7280))
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
TextButton(onClick = {}) {
|
|
||||||
Text("+ Ersten Sponsor hinzufügen", color = AccentBlue, fontSize = 13.sp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Aktions-Buttons ──────────────────────────────────────────────────
|
// ── Footer ──────────────────────────────────────────────────────────
|
||||||
Row(
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
OutlinedButton(onClick = {}) { Text("Zurücksetzen") }
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {},
|
onClick = { /* Speichern */ },
|
||||||
|
enabled = nrConfirmed && znsDataLoaded && kat.isNotEmpty() && von.isNotBlank() && titel.isNotBlank(),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||||
) { Text("Speichern") }
|
modifier = Modifier.padding(bottom = 24.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Save, null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Änderungen speichern")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dialog-Simulationen
|
||||||
|
if (showZnsDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showZnsDialog = false },
|
||||||
|
title = { Text("ZNS Import") },
|
||||||
|
text = { Text("Simuliere ZNS-Stammdaten Import für Turnier #$turnierNr...") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { znsDataLoaded = true; showZnsDialog = false }) { Text("Importieren") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showZnsDialog = false }) { Text("Abbrechen") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePickerVon) {
|
||||||
|
val state = rememberDatePickerState()
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePickerVon = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
state.selectedDateMillis?.let {
|
||||||
|
von = LocalDate.ofEpochDay(it / (24 * 60 * 60 * 1000)).toString()
|
||||||
|
}
|
||||||
|
showDatePickerVon = false
|
||||||
|
}) { Text("OK") }
|
||||||
|
}
|
||||||
|
) { DatePicker(state) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePickerBis) {
|
||||||
|
val state = rememberDatePickerState()
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePickerBis = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
state.selectedDateMillis?.let {
|
||||||
|
bis = LocalDate.ofEpochDay(it / (24 * 60 * 60 * 1000)).toString()
|
||||||
|
}
|
||||||
|
showDatePickerBis = false
|
||||||
|
}) { Text("OK") }
|
||||||
|
}
|
||||||
|
) { DatePicker(state) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SectionCard(
|
private fun SectionCard(
|
||||||
title: String,
|
title: String,
|
||||||
action: @Composable (() -> Unit)? = null,
|
content: @Composable ColumnScope.() -> Unit
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Row(
|
Text(title, style = MaterialTheme.typography.titleLarge, color = PrimaryBlue, fontWeight = FontWeight.Bold)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
|
||||||
action?.invoke()
|
|
||||||
}
|
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,20 +356,14 @@ private fun SectionCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FormRow(label: String, content: @Composable ColumnScope.() -> Unit) {
|
private fun FormRow(label: String, content: @Composable ColumnScope.() -> Unit) {
|
||||||
Row(
|
Row(Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.Top,
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
label,
|
||||||
fontSize = 13.sp,
|
|
||||||
modifier = Modifier.width(140.dp).padding(top = 12.dp),
|
modifier = Modifier.width(140.dp).padding(top = 12.dp),
|
||||||
color = Color(0xFF374151),
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
Column(
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -23,9 +23,7 @@ import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
|||||||
import at.mocode.ping.feature.presentation.PingScreen
|
import at.mocode.ping.feature.presentation.PingScreen
|
||||||
import at.mocode.ping.feature.presentation.PingViewModel
|
import at.mocode.ping.feature.presentation.PingViewModel
|
||||||
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
||||||
import at.mocode.turnier.feature.presentation.TurnierWizardV2
|
|
||||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
||||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore
|
|
||||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||||
@@ -388,7 +386,10 @@ private fun DesktopContentArea(
|
|||||||
// Turnier-Screens
|
// Turnier-Screens
|
||||||
is AppScreen.TurnierDetail -> {
|
is AppScreen.TurnierDetail -> {
|
||||||
val evtId = currentScreen.veranstaltungId
|
val evtId = currentScreen.veranstaltungId
|
||||||
if (!FakeVeranstaltungStore.exists(evtId)) {
|
val parent = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { v ->
|
||||||
|
at.mocode.desktop.v2.StoreV2.eventsFor(v.id).any { it.id == evtId }
|
||||||
|
}
|
||||||
|
if (parent == null) {
|
||||||
InvalidContextNotice(
|
InvalidContextNotice(
|
||||||
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
||||||
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
||||||
@@ -397,7 +398,7 @@ private fun DesktopContentArea(
|
|||||||
TurnierDetailScreen(
|
TurnierDetailScreen(
|
||||||
veranstaltungId = evtId,
|
veranstaltungId = evtId,
|
||||||
turnierId = currentScreen.turnierId,
|
turnierId = currentScreen.turnierId,
|
||||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,10 +414,11 @@ private fun DesktopContentArea(
|
|||||||
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TurnierWizardV2(
|
at.mocode.desktop.v2.TurnierWizardV2(
|
||||||
|
veranstalterId = parent.id,
|
||||||
veranstaltungId = evtId,
|
veranstaltungId = evtId,
|
||||||
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||||
onSave = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
onSaved = { _ -> onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -65,14 +65,19 @@ object StoreV2 {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Turniere für Neumarkt
|
|
||||||
TurnierStoreV2.add(
|
TurnierStoreV2.add(
|
||||||
neumarktId,
|
neumarktId,
|
||||||
TurnierV2(101, neumarktId, 26128, "CSN-C-NEU CSNP-C-NEU", "2026-04-10", "2026-04-12")
|
TurnierV2(101, neumarktId, 26128, datumVon = "2026-04-10", datumBis = "2026-04-12", znsDataLoaded = true).apply {
|
||||||
|
kategorie.add("CSN-C-NEU")
|
||||||
|
kategorie.add("CSNP-C-NEU")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
TurnierStoreV2.add(
|
TurnierStoreV2.add(
|
||||||
neumarktId,
|
neumarktId,
|
||||||
TurnierV2(102, neumarktId, 26129, "CDN-C-NEU CDNP-C-NEU", "2026-04-10", "2026-04-12")
|
TurnierV2(102, neumarktId, 26129, datumVon = "2026-04-10", datumBis = "2026-04-12", znsDataLoaded = true).apply {
|
||||||
|
kategorie.add("CDN-C-NEU")
|
||||||
|
kategorie.add("CDNP-C-NEU")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 2. Linz 2026 (ID 200)
|
// 2. Linz 2026 (ID 200)
|
||||||
@@ -88,7 +93,11 @@ object StoreV2 {
|
|||||||
beschreibung = "Großes Reit-Event am Ebelsberger Schlosspark."
|
beschreibung = "Großes Reit-Event am Ebelsberger Schlosspark."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
TurnierStoreV2.add(linzId, TurnierV2(201, linzId, 26500, "CSN-B*", "2026-05-20", "2026-05-24"))
|
TurnierStoreV2.add(
|
||||||
|
linzId,
|
||||||
|
TurnierV2(201, linzId, 26500, datumVon = "2026-05-20", datumBis = "2026-05-24", znsDataLoaded = true).apply {
|
||||||
|
kategorie.add("CSN-B*")
|
||||||
|
})
|
||||||
|
|
||||||
// 3. Ein historisches Event (ID 300)
|
// 3. Ein historisches Event (ID 300)
|
||||||
addEventFirst(
|
addEventFirst(
|
||||||
|
|||||||
+763
-93
@@ -4,17 +4,23 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -497,7 +503,10 @@ fun VeranstaltungKonfigV2(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val isDateRangeInvalid = dateVon != null && dateBis != null && dateBis.isBefore(dateVon)
|
val today = LocalDate.now()
|
||||||
|
val isStartInPast = dateVon != null && dateVon.isBefore(today)
|
||||||
|
val isDateRangeInvalid =
|
||||||
|
(dateVon != null && dateBis != null && dateBis.isBefore(dateVon)) || isStartInPast
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -506,10 +515,11 @@ fun VeranstaltungKonfigV2(
|
|||||||
label = { Text("Datum von") },
|
label = { Text("Datum von") },
|
||||||
modifier = Modifier.weight(1f).clickable { showDatePickerVon = true },
|
modifier = Modifier.weight(1f).clickable { showDatePickerVon = true },
|
||||||
enabled = false,
|
enabled = false,
|
||||||
|
isError = isStartInPast,
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
disabledBorderColor = MaterialTheme.colorScheme.outline,
|
disabledBorderColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline,
|
||||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
disabledLabelColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
|
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
),
|
),
|
||||||
@@ -517,6 +527,11 @@ fun VeranstaltungKonfigV2(
|
|||||||
IconButton(onClick = { showDatePickerVon = true }) {
|
IconButton(onClick = { showDatePickerVon = true }) {
|
||||||
Icon(Icons.Default.DateRange, contentDescription = "Datum wählen")
|
Icon(Icons.Default.DateRange, contentDescription = "Datum wählen")
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
supportingText = {
|
||||||
|
if (isStartInPast) {
|
||||||
|
Text("Startdatum darf nicht in der Vergangenheit liegen.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -643,7 +658,9 @@ fun VeranstaltungKonfigV2(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val rangeInvalid = dVon != null && dBis != null && dBis.isBefore(dVon)
|
val today2 = LocalDate.now()
|
||||||
|
val startInPast = dVon != null && dVon.isBefore(today2)
|
||||||
|
val rangeInvalid = (dVon != null && dBis != null && dBis.isBefore(dVon)) || startInPast
|
||||||
titel.isNotBlank() && von.isNotBlank() && !rangeInvalid
|
titel.isNotBlank() && von.isNotBlank() && !rangeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,9 +679,16 @@ data class TurnierV2(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
val veranstaltungId: Long,
|
val veranstaltungId: Long,
|
||||||
val turnierNr: Int,
|
val turnierNr: Int,
|
||||||
var kategorie: String,
|
var typ: String = "ÖTO (National)",
|
||||||
|
var znsDataLoaded: Boolean = false,
|
||||||
|
var sparten: SnapshotStateList<String> = mutableStateListOf(),
|
||||||
|
var klassen: SnapshotStateList<String> = mutableStateListOf(),
|
||||||
|
var kategorie: SnapshotStateList<String> = mutableStateListOf(),
|
||||||
var datumVon: String,
|
var datumVon: String,
|
||||||
var datumBis: String?,
|
var datumBis: String?,
|
||||||
|
var titel: String = "",
|
||||||
|
var subTitel: String = "",
|
||||||
|
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
object TurnierStoreV2 {
|
object TurnierStoreV2 {
|
||||||
@@ -684,48 +708,100 @@ fun VeranstaltungUebersichtV2(
|
|||||||
) {
|
) {
|
||||||
DesktopThemeV2 {
|
DesktopThemeV2 {
|
||||||
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
|
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
|
||||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
val turniere = remember(veranstaltungId) { TurnierStoreV2.list(veranstaltungId) }
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Icon(
|
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
// Header
|
||||||
contentDescription = "Zurück",
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
modifier = Modifier.clickable { onBack() })
|
IconButton(onClick = onBack) {
|
||||||
Text(veranstaltung?.titel ?: "Veranstaltung", style = MaterialTheme.typography.titleLarge)
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = veranstaltung?.titel ?: "Veranstaltung",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
if (veranstaltung != null) {
|
||||||
|
Text(
|
||||||
|
text = "${veranstaltung.ort} | ${veranstaltung.datumVon}${veranstaltung.datumBis?.let { " – $it" } ?: ""}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Button(onClick = onTurnierNeu) { Text("+ Neues Turnier") }
|
ElevatedButton(
|
||||||
|
onClick = onTurnierNeu,
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Neues Turnier")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (veranstaltung != null) {
|
// KPI Dashboard
|
||||||
Text("Zeitraum: ${veranstaltung.datumVon}${veranstaltung.datumBis?.let { " – $it" } ?: ""}")
|
Row(
|
||||||
Text("Status: ${veranstaltung.status}")
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
KpiCard(
|
||||||
|
title = "Turniere",
|
||||||
|
value = turniere.size.toString(),
|
||||||
|
icon = Icons.Default.Event,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
KpiCard(
|
||||||
|
title = "Nennungen",
|
||||||
|
value = if (veranstaltungId == 100L) "248" else "0",
|
||||||
|
icon = Icons.Default.Description,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
KpiCard(
|
||||||
|
title = "Reiter",
|
||||||
|
value = if (veranstaltungId == 100L) "112" else "0",
|
||||||
|
icon = Icons.Default.Person,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
KpiCard(
|
||||||
|
title = "Pferde",
|
||||||
|
value = if (veranstaltungId == 100L) "145" else "0",
|
||||||
|
icon = Icons.Default.Pets,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val list = remember(veranstaltungId) { TurnierStoreV2.list(veranstaltungId) }
|
// Turnierliste
|
||||||
if (list.isEmpty()) Text("Noch keine Turniere angelegt.", color = Color(0xFF6B7280))
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Text("Zugeordnete Turniere", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||||
|
if (turniere.isNotEmpty()) {
|
||||||
|
Badge(containerColor = MaterialTheme.colorScheme.secondaryContainer) {
|
||||||
|
Text(turniere.size.toString(), color = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
if (turniere.isEmpty()) {
|
||||||
items(list) { t ->
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Card(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
|
Text(
|
||||||
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
"Noch keine Turniere angelegt.",
|
||||||
Column(Modifier.weight(1f)) {
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
Text("Turnier ${t.turnierNr}", fontWeight = FontWeight.SemiBold)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Text(t.kategorie, color = Color(0xFF6B7280))
|
)
|
||||||
Text("${t.datumVon}${t.datumBis?.let { " – $it" } ?: ""}", color = Color(0xFF6B7280))
|
}
|
||||||
}
|
} else {
|
||||||
Button(onClick = { onTurnierOpen(t.id) }) { Text("Zum Turnier") }
|
LazyColumn(
|
||||||
Spacer(Modifier.width(8.dp))
|
modifier = Modifier.fillMaxSize(),
|
||||||
var confirm by remember { mutableStateOf(false) }
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
if (confirm) {
|
contentPadding = PaddingValues(bottom = 24.dp)
|
||||||
AlertDialog(
|
) {
|
||||||
onDismissRequest = { confirm = false },
|
items(turniere) { t ->
|
||||||
confirmButton = { TextButton(onClick = { TurnierStoreV2.remove(veranstaltungId, t.id); confirm = false }) { Text("Löschen") } },
|
TurnierCard(
|
||||||
dismissButton = { TextButton(onClick = { confirm = false }) { Text("Abbrechen") } },
|
turnier = t,
|
||||||
title = { Text("Turnier löschen?") },
|
onOpen = { onTurnierOpen(t.id) },
|
||||||
text = { Text("Dieses Turnier wird aus der Veranstaltung entfernt (Prototyp).") }
|
onDelete = { TurnierStoreV2.remove(veranstaltungId, t.id) }
|
||||||
)
|
)
|
||||||
}
|
|
||||||
IconButton(onClick = { confirm = true }) { Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626)) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,6 +809,109 @@ fun VeranstaltungUebersichtV2(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KpiCard(
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ElevatedCard(modifier = modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
shape = androidx.compose.foundation.shape.CircleShape,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Text(title, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TurnierCard(
|
||||||
|
turnier: TurnierV2,
|
||||||
|
onOpen: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = onOpen
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
"Turnier #${turnier.turnierNr}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
turnier.kategorie.forEach { kat ->
|
||||||
|
SuggestionChip(
|
||||||
|
onClick = {},
|
||||||
|
label = { Text(kat, fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "${turnier.datumVon}${turnier.datumBis?.let { " – $it" } ?: ""}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
TextButton(onClick = onOpen) {
|
||||||
|
Text("Öffnen")
|
||||||
|
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { showDeleteConfirm = true }) {
|
||||||
|
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDeleteConfirm) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteConfirm = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDelete()
|
||||||
|
showDeleteConfirm = false
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
|
||||||
|
) { Text("Löschen") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteConfirm = false }) { Text("Abbrechen") }
|
||||||
|
},
|
||||||
|
title = { Text("Turnier löschen?") },
|
||||||
|
text = { Text("Möchten Sie das Turnier #${turnier.turnierNr} wirklich löschen?") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TurnierWizardV2(
|
fun TurnierWizardV2(
|
||||||
veranstalterId: Long,
|
veranstalterId: Long,
|
||||||
@@ -742,70 +921,561 @@ fun TurnierWizardV2(
|
|||||||
) {
|
) {
|
||||||
DesktopThemeV2 {
|
DesktopThemeV2 {
|
||||||
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
|
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
|
||||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
var currentStep by remember { mutableStateOf(1) }
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
var showZnsDialog by remember { mutableStateOf(false) }
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
// State für alle Felder
|
||||||
contentDescription = "Zurück",
|
var nr by remember { mutableStateOf("") }
|
||||||
modifier = Modifier.clickable { onBack() })
|
var nrConfirmed by remember { mutableStateOf(false) }
|
||||||
Text("Neues Turnier", style = MaterialTheme.typography.titleLarge)
|
var znsDataLoaded by remember { mutableStateOf(false) }
|
||||||
|
var typ by remember { mutableStateOf("ÖTO (National)") }
|
||||||
|
|
||||||
|
val sparten = remember { mutableStateListOf<String>() }
|
||||||
|
val klassen = remember { mutableStateListOf<String>() }
|
||||||
|
val kat = remember { mutableStateListOf<String>() }
|
||||||
|
var von by remember { mutableStateOf(veranstaltung?.datumVon ?: "") }
|
||||||
|
var bis by remember { mutableStateOf(veranstaltung?.datumBis ?: "") }
|
||||||
|
|
||||||
|
var titel by remember { mutableStateOf("") }
|
||||||
|
var subTitel by remember { mutableStateOf("") }
|
||||||
|
val sponsoren = remember { mutableStateListOf<String>() }
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||||
|
// Header mit Breadcrumbs-Optik
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
|
}
|
||||||
|
Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
"Schritt $currentStep von 3",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var nr by remember { mutableStateOf("") }
|
LinearProgressIndicator(
|
||||||
var locked by remember { mutableStateOf(false) }
|
progress = { currentStep / 3f },
|
||||||
// Kategorie wird gemäß Neumarkt-Logik automatisch vorbelegt aus der Turnier-Nr.
|
modifier = Modifier.fillMaxWidth().height(8.dp).clip(RoundedCornerShape(4.dp)),
|
||||||
var kat by remember { mutableStateOf("") }
|
)
|
||||||
var von by remember { mutableStateOf(veranstaltung?.datumVon ?: "") }
|
|
||||||
var bis by remember { mutableStateOf(veranstaltung?.datumBis ?: "") }
|
|
||||||
|
|
||||||
OutlinedTextField(value = nr, onValueChange = {
|
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||||
if (!locked) nr = it.filter { ch -> ch.isDigit() }.take(5)
|
when (currentStep) {
|
||||||
}, label = { Text("Turnier‑Nr. (5‑stellig)") }, enabled = !locked, modifier = Modifier.fillMaxWidth())
|
1 -> Step1Basics(
|
||||||
|
nr, { nr = it },
|
||||||
|
nrConfirmed, { nrConfirmed = it },
|
||||||
|
typ, { typ = it },
|
||||||
|
znsDataLoaded, { znsDataLoaded = it }
|
||||||
|
)
|
||||||
|
|
||||||
val nrValid = nr.length == 5
|
2 -> Step2Sparten(
|
||||||
Button(onClick = {
|
sparten, klassen, kat,
|
||||||
// Auto-Mapping gemäß vorhandener Neumarkt-Dokumentation
|
von, { von = it }, bis, { bis = it },
|
||||||
kat = when (nr) {
|
veranstaltung
|
||||||
"26128" -> "CSN-C-NEU CSNP-C-NEU"
|
)
|
||||||
"26129" -> "CDN-C-NEU CDNP-C-NEU"
|
|
||||||
else -> ""
|
3 -> Step3Branding(titel, { titel = it }, subTitel, { subTitel = it }, sponsoren)
|
||||||
}
|
}
|
||||||
locked = true
|
}
|
||||||
}, enabled = nrValid && !locked) { Text("Nummer bestätigen & initialisieren") }
|
|
||||||
if (!nrValid) Text("Genau 5 Ziffern erforderlich", color = Color(0xFFB00020))
|
|
||||||
|
|
||||||
val freigeschaltet = locked
|
// Footer Navigation
|
||||||
// Kategorie-Auswahl gemäß Vorlage: Dropdown mit sinnvollen Optionen und Auto-Vorbelegung
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
val kategorien = remember { listOf("CDN-C-NEU CDNP-C-NEU", "CSN-C-NEU CSNP-C-NEU") }
|
OutlinedButton(
|
||||||
var katMenu by remember { mutableStateOf(false) }
|
onClick = { if (currentStep > 1) currentStep-- else onBack() }
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
) {
|
||||||
Text("Kategorie:")
|
Text(if (currentStep == 1) "Abbrechen" else "Zurück")
|
||||||
Box {
|
}
|
||||||
OutlinedButton(onClick = { if (freigeschaltet) katMenu = true }, enabled = freigeschaltet) {
|
|
||||||
Text(if (kat.isBlank()) "Kategorie wählen" else kat)
|
val canContinue = when (currentStep) {
|
||||||
|
1 -> nr.length == 5 && nrConfirmed && znsDataLoaded
|
||||||
|
2 -> {
|
||||||
|
val vVon = veranstaltung?.datumVon?.let { LocalDate.parse(it) }
|
||||||
|
val vBis = veranstaltung?.datumBis?.let { LocalDate.parse(it) }
|
||||||
|
val tVon = try {
|
||||||
|
LocalDate.parse(von)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val tBis = if (bis.isBlank()) tVon else try {
|
||||||
|
LocalDate.parse(bis)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateValid = if (vVon != null && tVon != null) {
|
||||||
|
val startOk = !tVon.isBefore(vVon)
|
||||||
|
val endOk = if (vBis != null && tBis != null) !tBis.isAfter(vBis) && !tBis.isBefore(tVon) else true
|
||||||
|
startOk && endOk
|
||||||
|
} else true
|
||||||
|
|
||||||
|
sparten.isNotEmpty() && klassen.isNotEmpty() && kat.isNotEmpty() && von.isNotBlank() && dateValid
|
||||||
}
|
}
|
||||||
DropdownMenu(expanded = katMenu, onDismissRequest = { katMenu = false }) {
|
|
||||||
kategorien.forEach { k ->
|
3 -> true
|
||||||
DropdownMenuItem(onClick = { kat = k; katMenu = false }, text = { Text(k) })
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (currentStep < 3) {
|
||||||
|
if (currentStep == 1) {
|
||||||
|
// Auto-Mapping bei Schritt-Wechsel
|
||||||
|
if (kat.isEmpty()) {
|
||||||
|
if (nr == "26128") {
|
||||||
|
if (!kat.contains("CSN-C-NEU")) kat.add("CSN-C-NEU")
|
||||||
|
if (!kat.contains("CSNP-C-NEU")) kat.add("CSNP-C-NEU")
|
||||||
|
}
|
||||||
|
if (nr == "26129") {
|
||||||
|
if (!kat.contains("CDN-C-NEU")) kat.add("CDN-C-NEU")
|
||||||
|
if (!kat.contains("CDNP-C-NEU")) kat.add("CDNP-C-NEU")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentStep++
|
||||||
|
} else {
|
||||||
|
val id = System.currentTimeMillis()
|
||||||
|
val newTurnier = TurnierV2(
|
||||||
|
id = id,
|
||||||
|
veranstaltungId = veranstaltungId,
|
||||||
|
turnierNr = nr.toInt(),
|
||||||
|
typ = typ,
|
||||||
|
znsDataLoaded = znsDataLoaded,
|
||||||
|
datumVon = von,
|
||||||
|
datumBis = bis.ifBlank { null },
|
||||||
|
titel = titel,
|
||||||
|
subTitel = subTitel
|
||||||
|
)
|
||||||
|
newTurnier.sparten.addAll(sparten)
|
||||||
|
newTurnier.klassen.addAll(klassen)
|
||||||
|
newTurnier.kategorie.addAll(kat)
|
||||||
|
newTurnier.sponsoren.addAll(sponsoren)
|
||||||
|
|
||||||
|
TurnierStoreV2.add(veranstaltungId, newTurnier)
|
||||||
|
onSaved(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = canContinue
|
||||||
|
) {
|
||||||
|
Text(if (currentStep == 3) "Turnier erstellen" else "Weiter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Step1Basics(
|
||||||
|
nr: String, onNrChange: (String) -> Unit,
|
||||||
|
nrConfirmed: Boolean, onNrConfirmedChange: (Boolean) -> Unit,
|
||||||
|
typ: String, onTypChange: (String) -> Unit,
|
||||||
|
znsDataLoaded: Boolean, onZnsDataLoadedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
var showImportProgress by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
|
Text("Turnier-Konfiguration", style = MaterialTheme.typography.titleLarge)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = nr,
|
||||||
|
onValueChange = {
|
||||||
|
onNrChange(it.filter { ch -> ch.isDigit() }.take(5))
|
||||||
|
onNrConfirmedChange(false)
|
||||||
|
},
|
||||||
|
label = { Text("Turnier-Nr. (z.B. 26128)") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = !nrConfirmed,
|
||||||
|
supportingText = { Text("5-stellige Nummer vom OePS") }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!nrConfirmed) {
|
||||||
|
Button(
|
||||||
|
onClick = { onNrConfirmedChange(true) },
|
||||||
|
enabled = nr.length == 5
|
||||||
|
) {
|
||||||
|
Text("Bestätigen")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.CheckCircle, "Bestätigt", tint = Color(0xFF4CAF50), modifier = Modifier.size(32.dp))
|
||||||
|
TextButton(onClick = { onNrConfirmedChange(false) }) { Text("Ändern") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text("Typ:", style = MaterialTheme.typography.labelLarge)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
listOf("ÖTO (National)", "FEI (International)").forEach { option ->
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onTypChange(option) }) {
|
||||||
|
RadioButton(selected = typ == option, onClick = { onTypChange(option) })
|
||||||
|
Text(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
if (typ.startsWith("ÖTO")) "Nationales Regelwerk kommt zum Einsatz" else "Internationales FEI-Reglement aktiv",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text("ZNS-Daten:", style = MaterialTheme.typography.labelLarge)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showImportProgress = true },
|
||||||
|
enabled = nrConfirmed && !znsDataLoaded
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.CloudDownload, null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Import via Internet")
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showImportProgress = true },
|
||||||
|
enabled = nrConfirmed && !znsDataLoaded
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Usb, null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Import via USB")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = if (znsDataLoaded) Color(0xFFE8F5E9) else Color(0xFFFFEBEE),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
if (znsDataLoaded) Icons.Default.CheckCircle else Icons.Default.Error,
|
||||||
|
null,
|
||||||
|
tint = if (znsDataLoaded) Color(0xFF2E7D32) else Color(0xFFC62828)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
if (znsDataLoaded) "ZNS-Daten geladen" else "Keine ZNS-Daten (Import erforderlich)",
|
||||||
|
color = if (znsDataLoaded) Color(0xFF2E7D32) else Color(0xFFC62828),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImportProgress) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { },
|
||||||
|
confirmButton = { },
|
||||||
|
title = { Text("ZNS Import") },
|
||||||
|
text = {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Daten werden verarbeitet...")
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
CircularProgressIndicator()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
onZnsDataLoadedChange(true)
|
||||||
|
showImportProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun Step2Sparten(
|
||||||
|
sparten: SnapshotStateList<String>,
|
||||||
|
klassen: SnapshotStateList<String>,
|
||||||
|
kat: SnapshotStateList<String>,
|
||||||
|
von: String, onVonChange: (String) -> Unit,
|
||||||
|
bis: String, onBisChange: (String) -> Unit,
|
||||||
|
veranstaltung: VeranstaltungV2?
|
||||||
|
) {
|
||||||
|
var showDatePickerVon by remember { mutableStateOf(false) }
|
||||||
|
var showDatePickerBis by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val vVon = veranstaltung?.datumVon?.let { LocalDate.parse(it) }
|
||||||
|
val vBis = veranstaltung?.datumBis?.let { LocalDate.parse(it) }
|
||||||
|
val tVon = try {
|
||||||
|
LocalDate.parse(von)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val tBis = if (bis.isBlank()) tVon else try {
|
||||||
|
LocalDate.parse(bis)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val isDateValid = if (vVon != null && tVon != null) {
|
||||||
|
val startOk = !tVon.isBefore(vVon)
|
||||||
|
val endOk = if (vBis != null && tBis != null) !tBis.isAfter(vBis) && !tBis.isBefore(tVon) else true
|
||||||
|
startOk && endOk
|
||||||
|
} else true
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
|
Text("Sparten & Klassen", style = MaterialTheme.typography.titleLarge)
|
||||||
|
|
||||||
|
// Datumsauswahl
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
"Zeitraum des Turniers:",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = von,
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text("Datum von") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showDatePickerVon = true }) {
|
||||||
|
Icon(Icons.Default.DateRange, contentDescription = "Kalender")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = bis,
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text("Datum bis (optional)") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showDatePickerBis = true }) {
|
||||||
|
Icon(Icons.Default.DateRange, contentDescription = "Kalender")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isError = !isDateValid && tBis != null && ((vBis != null && tBis.isAfter(vBis)) || (tVon != null && tBis.isBefore(
|
||||||
|
tVon
|
||||||
|
))),
|
||||||
|
supportingText = {
|
||||||
|
if (!isDateValid && tBis != null) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePickerVon) {
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = tVon?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||||
|
?: System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePickerVon = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
datePickerState.selectedDateMillis?.let {
|
||||||
|
onVonChange(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate().toString())
|
||||||
|
}
|
||||||
|
showDatePickerVon = false
|
||||||
|
}) { Text("OK") }
|
||||||
|
}
|
||||||
|
) { DatePicker(state = datePickerState) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePickerBis) {
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = tBis?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||||
|
?: tVon?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||||
|
?: System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePickerBis = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
datePickerState.selectedDateMillis?.let {
|
||||||
|
onBisChange(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate().toString())
|
||||||
|
}
|
||||||
|
showDatePickerBis = false
|
||||||
|
}) { Text("OK") }
|
||||||
|
}
|
||||||
|
) { DatePicker(state = datePickerState) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text("Sparten:", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
listOf("Dressur", "Springen").forEach { option ->
|
||||||
|
FilterChip(
|
||||||
|
selected = sparten.contains(option),
|
||||||
|
onClick = { if (sparten.contains(option)) sparten.remove(option) else sparten.add(option) },
|
||||||
|
label = { Text(option) },
|
||||||
|
leadingIcon = if (sparten.contains(option)) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text("Klassen:", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val allKlassen = listOf("C-NEU", "C", "B", "A", "L", "LM", "M", "S")
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
allKlassen.chunked(4).forEach { rowKlassen ->
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
rowKlassen.forEach { option ->
|
||||||
|
FilterChip(
|
||||||
|
selected = klassen.contains(option),
|
||||||
|
onClick = { if (klassen.contains(option)) klassen.remove(option) else klassen.add(option) },
|
||||||
|
label = { Text(option, modifier = Modifier.padding(horizontal = 8.dp)) },
|
||||||
|
leadingIcon = if (klassen.contains(option)) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
}
|
||||||
OutlinedTextField(value = von, onValueChange = { von = it }, label = { Text("von (YYYY-MM-DD)") }, enabled = freigeschaltet, modifier = Modifier.weight(1f))
|
|
||||||
OutlinedTextField(value = bis, onValueChange = { bis = it }, label = { Text("bis (YYYY-MM-DD)") }, enabled = freigeschaltet, modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
val parentVon = veranstaltung?.datumVon
|
|
||||||
val parentBis = veranstaltung?.datumBis
|
|
||||||
val dateOk = freigeschaltet && von.isNotBlank() && (bis.isBlank() || bis >= von) &&
|
|
||||||
(parentVon == null || (von >= parentVon && (parentBis == null || (bis.isBlank() || bis <= parentBis))))
|
|
||||||
if (freigeschaltet && !dateOk) Text("Turnier-Datum muss im Veranstaltungszeitraum liegen", color = Color(0xFFB00020))
|
|
||||||
|
|
||||||
Button(onClick = {
|
Column {
|
||||||
val id = System.currentTimeMillis()
|
Text(
|
||||||
TurnierStoreV2.add(veranstaltungId, TurnierV2(id, veranstaltungId, nr.toInt(), kat, von, bis.ifBlank { null }))
|
"Kategorie (Vorschläge):",
|
||||||
onSaved(id)
|
style = MaterialTheme.typography.labelLarge,
|
||||||
}, enabled = freigeschaltet && nrValid && kat.isNotBlank() && dateOk) { Text("Speichern") }
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val suggestions = remember(sparten.toList(), klassen.toList()) {
|
||||||
|
val list = mutableListOf<String>()
|
||||||
|
sparten.forEach { s ->
|
||||||
|
val prefix = if (s == "Dressur") "CDN" else "CSN"
|
||||||
|
|
||||||
|
klassen.forEach { k ->
|
||||||
|
val suffix = if (k == "C-NEU") "-C-NEU" else "-$k"
|
||||||
|
list.add("$prefix$suffix")
|
||||||
|
list.add("${prefix}P$suffix")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sparten.isEmpty() || klassen.isEmpty()) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(60.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text("Bitte Sparte(n) und Klasse(n) auswählen", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
suggestions.chunked(4).forEach { chunk ->
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
chunk.forEach { suggestion ->
|
||||||
|
InputChip(
|
||||||
|
selected = kat.contains(suggestion),
|
||||||
|
onClick = { if (kat.contains(suggestion)) kat.remove(suggestion) else kat.add(suggestion) },
|
||||||
|
label = { Text(suggestion) },
|
||||||
|
trailingIcon = if (kat.contains(suggestion)) {
|
||||||
|
{ Icon(Icons.Default.Check, null, modifier = Modifier.size(18.dp)) }
|
||||||
|
} else null,
|
||||||
|
colors = InputChipDefaults.inputChipColors(
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Step3Branding(
|
||||||
|
titel: String, onTitelChange: (String) -> Unit,
|
||||||
|
subTitel: String, onSubTitelChange: (String) -> Unit,
|
||||||
|
sponsoren: SnapshotStateList<String>
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
|
Text("Turnier-Beschreibung", style = MaterialTheme.typography.titleLarge)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = titel,
|
||||||
|
onValueChange = onTitelChange,
|
||||||
|
label = { Text("Titel (z.B. Frühjahrs-Turnier 2026)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = subTitel,
|
||||||
|
onValueChange = onSubTitelChange,
|
||||||
|
label = { Text("Sub-Titel (z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text("Sponsoren", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
TextButton(onClick = { sponsoren.add("") }) {
|
||||||
|
Icon(Icons.Default.Add, null)
|
||||||
|
Text("Sponsor hinzufügen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sponsoren.isEmpty()) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text("Noch keine Sponsoren hinzugefügt", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sponsoren.forEachIndexed { index, sponsor ->
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = sponsor,
|
||||||
|
onValueChange = { sponsoren[index] = it },
|
||||||
|
label = { Text("Sponsor Name") },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
IconButton(onClick = { sponsoren.removeAt(index) }) {
|
||||||
|
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user