Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f44b2c8126 | |||
| 496e801943 | |||
| 1699c24875 | |||
| 683ef956fc | |||
| 6bbf6dc966 | |||
| 94306329c9 | |||
| 659e699c33 | |||
| 96c9abb264 | |||
| ad529f7395 | |||
| b2a0883388 | |||
| 442caa59ff | |||
| 340f341594 | |||
| 5980fbe14f | |||
| 2d532eb41c | |||
| c20899752a | |||
| 5065febca2 |
+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)
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Frontend-Komponenten Roadmap: Meldestelle-Biest
|
||||||
|
|
||||||
|
🏗️ **[Lead Architect]** | 31. März 2026
|
||||||
|
|
||||||
|
Diese Roadmap definiert den Weg von der aktuellen Prototypen-Phase hin zu einer professionellen, konsistenten und
|
||||||
|
performanten Desktop-App. Wir setzen auf einen komponentengetriebenen Ansatz (High-Density UI), um die komplexe
|
||||||
|
Datenverwaltung der Turniermeldestelle effizient abzubilden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Cleanup & Konsolidierung (Das Fundament) ✅ [ABGESCHLOSSEN]
|
||||||
|
|
||||||
|
Bevor wir neue Features bauen, räumen wir die bestehenden Entwürfe auf, um Redundanzen zu vermeiden.
|
||||||
|
|
||||||
|
* [x] **Design-System Refactoring:**
|
||||||
|
* [x] `Buttons.kt` (DenseButton) in `MsButton.kt` überführt.
|
||||||
|
* [x] Einheitliches Naming: Alle Basis-Komponenten erhalten das Präfix `Ms` (z.B. `MsButton.kt`, `MsTextField.kt`).
|
||||||
|
* [x] Redundante Placeholder-Dateien entfernt oder in `core/design-system/models/` bündeln.
|
||||||
|
* [x] **Theme-Check:**
|
||||||
|
* [x] Sicherstellen, dass alle Farben aus `AppColors` kommen und nicht hart codiert sind.
|
||||||
|
* [x] Typografie-Skalen für High-Density optimieren (LabelSmall für Tabellen).
|
||||||
|
* [x] **Build-Fixes:**
|
||||||
|
* [x] Referenzen in `ping-feature` korrigiert.
|
||||||
|
* [x] Referenzen in `profile-feature` korrigiert.
|
||||||
|
|
||||||
|
## Phase 2: Daten-Visualisierungs-Komponenten (Das Herzstück) ✅ [ABGESCHLOSSEN]
|
||||||
|
|
||||||
|
Turniermanagement bedeutet Arbeit mit Listen. Wir benötigen mächtige, aber kompakte Anzeige-Komponenten.
|
||||||
|
|
||||||
|
* [x] **`MsDataTable`:**
|
||||||
|
* [x] KMP-kompatible Tabelle mit Sticky Header.
|
||||||
|
* [x] Generische Spaltendefinition mit Custom Cell Renderern.
|
||||||
|
* [x] Zeilen-Selektion (Einzel-Klick) und gestreiftes Zeilen-Design.
|
||||||
|
* [x] **`MsStatusBadge`:**
|
||||||
|
* [x] Farbliche Kodierung für Nennungsstatus, Lizenzstatus und Prüfungsstatus.
|
||||||
|
* [x] Kompaktes Design für die Nutzung innerhalb von Tabellenzellen.
|
||||||
|
* [x] **`MsFilterBar`:**
|
||||||
|
* [x] Suchfeld mit Debounce-Unterstützung (Pattern-basiert).
|
||||||
|
* [x] Filter-Chips für schnelle Status-Wechsel.
|
||||||
|
* [x] Anzeige der Trefferanzahl (Result Count).
|
||||||
|
|
||||||
|
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) ✅ [ABGESCHLOSSEN]
|
||||||
|
|
||||||
|
Eingabe von Stammdaten muss schnell und fehlerfrei erfolgen.
|
||||||
|
|
||||||
|
* [x] **`MsEnumDropdown`:** Automatisches Mapping von Backend-Enums (ÖTO) auf UI-Auswahl.
|
||||||
|
* [x] **`MsValidationWrapper`:** Konsistente Anzeige von Fehlern und Warnungen (z.B. ÖTO-Validierungsregeln).
|
||||||
|
* [x] **`MsSearchableSelect`:** Für die Verknüpfung von Reitern/Pferden (Autocomplete-Suche).
|
||||||
|
|
||||||
|
## Phase 4: Layout-Patterns & Navigation ✅ [ABGESCHLOSSEN]
|
||||||
|
|
||||||
|
Hier bringen wir alles zusammen, bevor das finale Routing implementiert wird.
|
||||||
|
|
||||||
|
* [x] **`MsMasterDetailLayout`:** Standard-Layout für alle Stammdaten-Screens (Liste & Editor).
|
||||||
|
* [x] **`MsActionToolbar`:** Einheitliche Platzierung von Hauptaktionen (Neu, Speichern, Drucken).
|
||||||
|
* [x] **`MsDialogShell`:** Standardisierter Rahmen für Modale und Bestätigungsdialoge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Routing & Screen-Komposition ✅ [ABGESCHLOSSEN]
|
||||||
|
|
||||||
|
In dieser Phase werden die Komponenten zu echten Features zusammengebaut.
|
||||||
|
|
||||||
|
* [x] **Reiter-Verwaltung (MVP):** Erster Screen mit `MsMasterDetailLayout`, `MsDataTable` und Editor.
|
||||||
|
* [x] **Pferde-Verwaltung (MVP):** Analog zur Reiter-Verwaltung (Fertiggestellt).
|
||||||
|
* [x] **Layout-Refactoring:** Umstellung auf Event-First Workflow (Login-Skip).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Vernetzung & Inter-App Kommunikation 🔵 [IN ARBEIT]
|
||||||
|
|
||||||
|
Nachdem die UI-Bausteine stehen, vernetzen wir die Desktop-Apps im LAN.
|
||||||
|
|
||||||
|
* [x] **Konzept & ADR:** ADR-0020 (LAN-Communication & Isolation) erstellt.
|
||||||
|
* [ ] **Discovery:** mDNS Integration für automatische Gerätefindung.
|
||||||
|
* [ ] **Sync:** WebSocket-basierte Echtzeit-Synchronisation zwischen Meldestelle und Richter.
|
||||||
|
* [ ] **Chat:** Implementierung des veranstaltungsweiten Chat-Fensters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erfolgs-Metriken
|
||||||
|
|
||||||
|
* **Wiederverwendbarkeit:** > 80% der UI besteht aus `Ms`-Komponenten.
|
||||||
|
* **Density:** Alle relevanten Daten eines Reiters/Pferdes sind ohne Scrollen in der Detailansicht sichtbar.
|
||||||
|
* **Performance:** `MsDataTable` rendert 500+ Zeilen flüssig auf ARM64 (Zora/Mac/Linux).
|
||||||
|
|
||||||
|
---
|
||||||
|
🧹 **[Curator]** | 2026-03-31
|
||||||
|
*Dieses Dokument dient als Single Source of Truth für die Frontend-Entwicklung.*
|
||||||
@@ -186,7 +186,18 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
|
|
||||||
## 4. Geplante Phasen
|
## 4. Geplante Phasen
|
||||||
|
|
||||||
### PHASE 7: Series-Context & Erweiterungen 🔵 PHASE 2+
|
### PHASE 7: Desktop-Vernetzung & Event-First Workflow 🔵 IN ARBEIT
|
||||||
|
|
||||||
|
*Ziel: LAN-Kommunikation zwischen Apps und Fokus auf Veranstaltungs-Verwaltung.*
|
||||||
|
|
||||||
|
* [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+
|
||||||
|
|
||||||
*Ziel: Cups, Serien und Meisterschaften mit konfigurierbaren Reglements.*
|
*Ziel: Cups, Serien und Meisterschaften mit konfigurierbaren Reglements.*
|
||||||
|
|
||||||
@@ -213,7 +224,8 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
| 11 | Masterdata: Importer-Einbettung als Worker | ✅ | ADR-0017 |
|
| 11 | Masterdata: Importer-Einbettung als Worker | ✅ | ADR-0017 |
|
||||||
| 12 | Masterdata: Rule-Versionierung (Regulation-as-Data) | ✅ | ADR-0018 |
|
| 12 | Masterdata: Rule-Versionierung (Regulation-as-Data) | ✅ | ADR-0018 |
|
||||||
| 13 | Masterdata: API-Schichten (REST vs. Ingestion) | ✅ | ADR-0019 |
|
| 13 | Masterdata: API-Schichten (REST vs. Ingestion) | ✅ | ADR-0019 |
|
||||||
| 14 | Masterdata: Observability & Operations | ✅ | masterdata-ops.md, CHANGELOG |
|
| 14 | Lokale Netzwerk-Kommunikation und Daten-Isolierung | ✅ | ADR-0020 |
|
||||||
|
| 15 | Masterdata: Observability & Operations | ✅ | masterdata-ops.md, CHANGELOG |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
type: ADR
|
||||||
|
status: ACCEPTED
|
||||||
|
owner: Lead Architect
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR-0020: Lokale Netzwerk-Kommunikation und Daten-Isolierung
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Akzeptiert (März 2026)
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Die Desktop-Anwendung ("Meldestelle-Biest") muss in einer lokalen Netzwerkumgebung (LAN) auf Reitturnieren
|
||||||
|
funktionieren, oft ohne zuverlässige Internetverbindung. Dabei müssen verschiedene Instanzen der App (z.B. "
|
||||||
|
Meldestelle-Desk-App" und "Richter-Desk-App") in Echtzeit miteinander kommunizieren.
|
||||||
|
|
||||||
|
Herausforderungen:
|
||||||
|
|
||||||
|
1. Wie finden sich die Geräte automatisch im LAN?
|
||||||
|
2. Wie wird die Sicherheit der Datenübertragung ohne zentrale Cloud-Infrastruktur gewährleistet?
|
||||||
|
3. Wie können Daten zwischen verschiedenen Turnieren innerhalb einer Veranstaltung sauber isoliert werden, während ein
|
||||||
|
veranstaltungsweiter Austausch (z.B. Chat) möglich bleibt?
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
Wir implementieren ein Peer-to-Peer (P2P) Kommunikationsmodell basierend auf folgenden Säulen:
|
||||||
|
|
||||||
|
1. **Discovery (Geräte finden):** Einsatz von `mDNS` (Multicast DNS / ZeroConf), damit sich Instanzen im lokalen
|
||||||
|
Netzwerk ohne manuelle IP-Konfiguration finden können.
|
||||||
|
2. **Transport & Echtzeit:** Nutzung von verschlüsselten **WebSockets** für die bidirektionale Datenübertragung in
|
||||||
|
Echtzeit.
|
||||||
|
3. **Sicherheit:** Verwendung eines **Shared Security Keys** (Sicherheitsschlüssel), der pro Veranstaltung vergeben
|
||||||
|
wird. Dieser dient als Basis für die Verschlüsselung und Authentifizierung der Peers.
|
||||||
|
4. **Daten-Isolierung (Namespacing):**
|
||||||
|
- Alle fachlichen Datenpakete (Nennungen, Ergebnisse) müssen zwingend eine `turnierId` im Header führen.
|
||||||
|
- Clients abonnieren spezifische Turnier-Streams. Daten anderer Turniere werden auf Protokollebene verworfen.
|
||||||
|
5. **Veranstaltungsweiter Chat:**
|
||||||
|
- Einführung eines globalen Kanals basierend auf der `veranstaltungId`.
|
||||||
|
- Nachrichten in diesem Kanal haben keine Turnier-Bindung und sind für alle Geräte der Veranstaltung sichtbar.
|
||||||
|
- Implementierung eines lokalen Nachrichtenspeichers (Buffering), um Offline-Phasen zu überbrücken.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
### Positiv
|
||||||
|
|
||||||
|
- **Offline-First:** Volle Funktionalität im LAN ohne Internetzwang.
|
||||||
|
- **Benutzerfreundlichkeit:** Automatische Geräteerkennung spart Zeit beim Setup vor Ort.
|
||||||
|
- **Datenintegrität:** Strikte Trennung der Turniere verhindert Fehlbuchungen.
|
||||||
|
- **Flexibilität:** Richter und Meldestelle können nahtlos zusammenarbeiten.
|
||||||
|
|
||||||
|
### Negativ/Herausforderungen
|
||||||
|
|
||||||
|
- **Netzwerk-Komplexität:** Handhabung von Firewall-Regeln und MDNS-Einschränkungen in manchen Router-Konfigurationen.
|
||||||
|
- **Synchronisation:** Konfliktauflösung bei gleichzeitigen Änderungen (Eventual Consistency) muss implementiert werden.
|
||||||
|
- **Ressourcen:** Laufende WebSocket-Verbindungen und MDNS-Services benötigen (geringe) zusätzliche Systemressourcen.
|
||||||
|
|
||||||
|
## Betrachtete Alternativen
|
||||||
|
|
||||||
|
- **Zentraler lokaler Server:** Erfordert ein dediziertes "Server-Gerät" und erhöht die Komplexität des Deployments. (
|
||||||
|
Verworfen zugunsten von P2P/Master-Client Hybrid).
|
||||||
|
- **HTTP Polling:** Zu langsam für Echtzeit-Richter-Eingaben und verursacht unnötigen Overhead. (Verworfen zugunsten von
|
||||||
|
WebSockets).
|
||||||
|
- **Manuelle IP-Eingabe:** Zu fehleranfällig für Endanwender unter Zeitdruck. (Verworfen zugunsten von mDNS).
|
||||||
|
|
||||||
|
## Referenzen
|
||||||
|
|
||||||
|
- Vision_03: Desktop-App Architektur
|
||||||
|
- [ADR-0004: Event-driven Communication](0004-event-driven-communication-de.md)
|
||||||
|
- [ADR-0008: Multiplatform Client Applications](0008-multiplatform-client-applications-de.md)
|
||||||
@@ -17,5 +17,6 @@ Namensschema: ADR-XXX-title.md mit fortlaufender Nummerierung.
|
|||||||
- ADR-0014 Bounded Context Mapping & Aggregate Roots
|
- ADR-0014 Bounded Context Mapping & Aggregate Roots
|
||||||
- ADR-0015 Context Map & Integration Patterns
|
- ADR-0015 Context Map & Integration Patterns
|
||||||
- ADR-0016 API-Design & Anti-Corruption Layer (ACL)
|
- ADR-0016 API-Design & Anti-Corruption Layer (ACL)
|
||||||
|
- ADR-0020 Lokale Netzwerk-Kommunikation und Daten-Isolierung
|
||||||
|
|
||||||
Siehe Template: ADR-000-template.md.
|
Siehe Template: ADR-000-template.md.
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
## 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
|
||||||
|
|
||||||
|
- **Fehlerbehebung Desktop-Shell Build:**
|
||||||
|
- **`VereinViewModel.kt`:** Das ViewModel erbt nun korrekt von `androidx.lifecycle.ViewModel`. Dies behebt einen "
|
||||||
|
Intersection Type" Fehler in `DesktopMainLayout.kt`, der beim Aufruf von `koinViewModel()` auftrat.
|
||||||
|
- **`VereinFeatureModule.kt`:** Die Koin-Konfiguration wurde wieder auf den Standard `viewModelOf(::VereinViewModel)`
|
||||||
|
umgestellt, da das ViewModel nun die korrekte Basisklasse besitzt.
|
||||||
|
- **Verifikation:** Die Desktop-Shell (`:frontend:shells:meldestelle-desktop`) kompiliert nun wieder fehlerfrei.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 15:30
|
||||||
|
|
||||||
|
- **Fehlerbehebung `verein-feature`:**
|
||||||
|
- **`VereinScreens.kt`:** Korrektur des `MsFilterBar`-Aufrufs. Der Parameter `onAddClick` wurde durch einen `actions`
|
||||||
|
Block mit einer `MsButton`-Komponente ersetzt, um dem Design-System zu entsprechen.
|
||||||
|
- **Verifikation:** Erfolgreicher Build des Moduls via `./gradlew :frontend:features:verein-feature:compileKotlinJvm`.
|
||||||
|
|
||||||
|
# Session Log: Event-First Workflow & UX-Polish (Initialer Schliff)
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Im Rahmen der MVP-Phase wurde der Fokus auf den "Event-First" Workflow gelegt. Ziel ist es, dass die App direkt mit der
|
||||||
|
Turnierverwaltung (Offline-First) startet, ohne den Nutzer durch ein separates Onboarding oder Login zu zwingen, solange
|
||||||
|
er lokal arbeitet. Zudem wurde eine konsistente Vereinsverwaltung gefordert, analog zu Reitern und Pferden.
|
||||||
|
|
||||||
|
## Durchgeführte Änderungen
|
||||||
|
|
||||||
|
### 1. Navigation & App-Start
|
||||||
|
|
||||||
|
- **Direkter Einstieg:** Die App startet nun direkt im Screen `AppScreen.Veranstaltungen`.
|
||||||
|
- **Anpassung DesktopApp.kt:** Das Login-Gate wurde so erweitert, dass alle für den Turnier-Workflow relevanten
|
||||||
|
Screens (Veranstaltungen, Veranstalter, Turniere, Vereine) auch ohne Authentifizierung zugänglich sind.
|
||||||
|
|
||||||
|
### 2. Veranstaltungen-Übersicht (Gesamtliste)
|
||||||
|
|
||||||
|
- **Neuer Screen `VeranstaltungenUebersichtV2`:** Implementierung einer zentralen Übersicht, die alle im lokalen Store
|
||||||
|
vorhandenen Veranstaltungen über alle Veranstalter hinweg anzeigt.
|
||||||
|
- **KPI-Kacheln:** Erweiterung um eine Kachel "VEREINE", die als Schnelleinstieg zur Vereinsverwaltung dient.
|
||||||
|
|
||||||
|
### 3. Vereins-Feature (Neu)
|
||||||
|
|
||||||
|
- **Neues Modul `verein-feature`:** Analog zu `reiter-feature` und `pferde-feature` wurde ein dediziertes Modul für die
|
||||||
|
Vereinsverwaltung erstellt.
|
||||||
|
- **Funktionalität:**
|
||||||
|
- **Domain:** Modell `Verein` mit Feldern für Name, Langname, OePS-Nr, Ort, PLZ und Status.
|
||||||
|
- **Presentation:** `VereinViewModel` (mit Such- und Filterlogik) und `VereinScreen` (Master-Detail-Layout).
|
||||||
|
- **Integration:** Koin-Modul `vereinFeatureModule` registriert und Navigation in `DesktopMainLayout.kt` integriert (
|
||||||
|
inkl. Breadcrumbs).
|
||||||
|
|
||||||
|
### 4. Integriertes Onboarding (Wizard)
|
||||||
|
|
||||||
|
- **Wizard-Erweiterung:** Das Geräte-Onboarding (Name & Sicherheitsschlüssel) wurde direkt in den
|
||||||
|
`VeranstaltungKonfigV2`-Wizard integriert. Nutzer müssen die Hardware-Informationen erst angeben, wenn sie die erste
|
||||||
|
Veranstaltung anlegen wollen.
|
||||||
|
|
||||||
|
### 4. Testdaten (Seed)
|
||||||
|
|
||||||
|
- **StoreV2.seed():** Es wurden realistische Testdaten für "Neumarkt 2026" und "Linz 2026" inklusive zugehöriger
|
||||||
|
Turniere angelegt, um den Workflow sofort testbar zu machen.
|
||||||
|
- **Stammdaten:** Hinzufügen von `oepsStammdaten` (Mock-Vereine) im `StoreV2` für die Suche im Anlage-Prozess.
|
||||||
|
|
||||||
|
## Betroffene Dateien
|
||||||
|
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt`
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt`
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt` (Neu:
|
||||||
|
`VeranstaltungenUebersichtV2`, `VeranstalterAnlegenWizard`)
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Stores.kt` (Neu: `allEvents()`, `seed()`,
|
||||||
|
`oepsStammdaten`)
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt` (Aufruf `seed()`)
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
- [ ] Verifikation der Detail-Ansicht für Turniere.
|
||||||
|
- [ ] Implementierung der mDNS Discovery für die lokale Vernetzung.
|
||||||
|
- [ ] ADR für das PDF-Rendering entwerfen.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 14:55
|
||||||
|
|
||||||
|
- **Datumswahl-Optimierung:** In `VeranstaltungKonfigV2` wurden die Textfelder für das Start- und Enddatum durch
|
||||||
|
Material 3 `DatePickerDialoge` ersetzt.
|
||||||
|
- **Interaktion:** Die Felder sind nun schreibgeschützt und öffnen bei Klick (oder Klick auf das Kalender-Icon) einen
|
||||||
|
grafischen Kalender.
|
||||||
|
- **Validierung:** Eine Logik wurde implementiert, die sicherstellt, dass das Enddatum nicht vor dem Startdatum liegen
|
||||||
|
kann. Falls dies der Fall ist, wird das Feld rot markiert und eine Fehlermeldung angezeigt.
|
||||||
|
- **Button-Status:** Der "Weiter"-Button in Schritt 2 ist nur aktiv, wenn Titel und Startdatum gesetzt sind und der
|
||||||
|
Datumsbereich gültig ist.
|
||||||
|
- **Technik:** Nutzung von `java.time.LocalDate` und `DateTimeFormatter.ISO_LOCAL_DATE` für konsistente
|
||||||
|
Datumsverarbeitung auf der JVM.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 14:45
|
||||||
|
|
||||||
|
- **Neuer Wizard "Veranstalter anlegen":** Ein 2-stufiger Prozess zur Erfassung neuer Vereine.
|
||||||
|
- **Schritt 1: Stammdaten-Suche:** Suche in `oepsStammdaten` nach Name, Ort oder OEPS-Nummer.
|
||||||
|
- **Schritt 2: Datenbestätigung:** Übernahme der Daten aus den Stammdaten oder manuelle Erfassung/Korrektur.
|
||||||
|
- **Flow-Optimierung:** Nach dem Anlegen eines neuen Veranstalters im `VeranstaltungKonfigV2`-Wizard springt die App nun
|
||||||
|
automatisch zu "Schritt 2: Basisdaten der Veranstaltung".
|
||||||
|
- **UI-Cleanup:** Import von `Icons.Default.Close` für den Abbrechen-Button im neuen Wizard.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 14:15
|
||||||
|
|
||||||
|
- **Neuer Wizard "Veranstaltung anlegen":** Der Prozess wurde in einen 3-stufigen Wizard umgewandelt.
|
||||||
|
- **Schritt 1: Veranstalterwahl:** Suche in bestehenden Vereinen oder Neuanlage eines Vereins direkt im Wizard.
|
||||||
|
- **Schritt 2: Basisdaten:** Titel, Untertitel, Datum von/bis und Austragungsort.
|
||||||
|
- **Schritt 3: Zusatzdaten & Branding:** Logo-URL/Pfad und Sponsoren-Liste (mit Live-Vorschau der Chips).
|
||||||
|
- **Modell-Erweiterung:** `VeranstaltungV2` wurde um `ort`, `untertitel`, `logoUrl` und eine reaktive Liste von
|
||||||
|
`sponsoren` erweitert.
|
||||||
|
- **Navigation:** Die `VeranstaltungKonfig` in `AppScreen` erlaubt nun eine optionale `veranstalterId`. Falls keine
|
||||||
|
übergeben wird (Aufruf aus Cockpit), startet der Wizard bei Schritt 1 (Veranstalterwahl).
|
||||||
|
- **UI-Polish:** Einsatz von `LinearProgressIndicator` für den Fortschritt und `Surface`-Karten für die Vereinsauswahl.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 13:55
|
||||||
|
|
||||||
|
- **Suche & Filter:** In der `VeranstaltungenUebersichtV2` wurde eine Suchfunktion (Titel/Verein) und ein
|
||||||
|
Status-Filter (via Filter-Chips) implementiert.
|
||||||
|
- **Datenmodell:** `VeranstaltungV2` wurde um ein Feld `beschreibung` erweitert.
|
||||||
|
- **UI-Anpassung:** Die Beschreibung wird nun in der Liste unter dem Titel/Verein angezeigt, um mehr Kontext zu bieten.
|
||||||
|
Status-Badges wurden für bessere Lesbarkeit auf `Surface` mit `primaryContainer` umgestellt.
|
||||||
|
|
||||||
|
## Nachtrag 31.03.2026 13:45
|
||||||
|
|
||||||
|
- **TopBar-Anpassung:** Der Root-Link "🏠 Admin - Verwaltung" wurde in "Veranstaltungen" umbenannt.
|
||||||
|
- **UI-Cleanup:** Der Logout-Button wurde aus der TopBar entfernt, da die App primär im Offline-First/Lokal-Modus
|
||||||
|
betrieben wird.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
-44
@@ -1,44 +0,0 @@
|
|||||||
package at.mocode.frontend.core.designsystem.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ein kompakter Button für unsere High-Density UI.
|
|
||||||
*
|
|
||||||
* Warum ein eigener Button?
|
|
||||||
* Der Standard Material3 Button ist sehr hoch (40dp+) und hat viel Padding.
|
|
||||||
* Das verschwendet Platz in Tabellen oder Toolbars.
|
|
||||||
* Unser 'DenseButton' ist fix 32dp hoch- und hat weniger Innenabstand.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun DenseButton(
|
|
||||||
text: String,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
containerColor: Color = MaterialTheme.colorScheme.primary
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
|
||||||
modifier = modifier.height(32.dp), // Fixe, kompakte Höhe
|
|
||||||
shape = MaterialTheme.shapes.small, // Nutzt unsere 4dp Rundung
|
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = containerColor),
|
|
||||||
contentPadding = PaddingValues(horizontal = Dimens.SpacingM, vertical = 0.dp) // Wenig Padding
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.labelMedium // Kleinere Schrift
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einheitliche Aktionsleiste für Editoren und Detail-Ansichten.
|
||||||
|
*
|
||||||
|
* @param onSave Callback für "Speichern".
|
||||||
|
* @param onCancel Callback für "Abbrechen".
|
||||||
|
* @param onDelete Callback für "Löschen" (Optional).
|
||||||
|
* @param onAdd Callback für "Neu" (Optional).
|
||||||
|
* @param extraActions Zusätzliche Aktionen (Optional, z.B. Drucken).
|
||||||
|
* @param isSaving Zeigt den Ladezustand beim Speichern an.
|
||||||
|
* @param canSave Steuert die Aktivierung des Speichern-Buttons.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsActionToolbar(
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onDelete: (() -> Unit)? = null,
|
||||||
|
onAdd: (() -> Unit)? = null,
|
||||||
|
extraActions: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
isSaving: Boolean = false,
|
||||||
|
canSave: Boolean = true,
|
||||||
|
title: String? = null
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
// --- 1. Titel-Bereich (Optional) ---
|
||||||
|
if (title != null) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.width(1.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Button-Bereich ---
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||||
|
) {
|
||||||
|
// Extra Aktionen (z.B. Drucken)
|
||||||
|
extraActions?.invoke(this)
|
||||||
|
|
||||||
|
if (extraActions != null && (onAdd != null || onDelete != null)) {
|
||||||
|
VerticalDivider(modifier = Modifier.height(24.dp).padding(horizontal = 4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neu / Löschen
|
||||||
|
if (onAdd != null) {
|
||||||
|
MsButton(
|
||||||
|
text = "Neu",
|
||||||
|
onClick = onAdd,
|
||||||
|
variant = ButtonVariant.OUTLINE,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDelete != null) {
|
||||||
|
MsButton(
|
||||||
|
text = "Löschen",
|
||||||
|
onClick = onDelete,
|
||||||
|
variant = ButtonVariant.TEXT,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalDivider(modifier = Modifier.height(24.dp).padding(horizontal = 4.dp))
|
||||||
|
|
||||||
|
// Hauptaktionen: Abbrechen & Speichern
|
||||||
|
MsButton(
|
||||||
|
text = "Abbrechen",
|
||||||
|
onClick = onCancel,
|
||||||
|
variant = ButtonVariant.TEXT,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
text = "Speichern",
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = canSave,
|
||||||
|
isLoading = isSaving,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-6
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
enum class ButtonVariant {
|
enum class ButtonVariant {
|
||||||
@@ -17,7 +18,7 @@ enum class ButtonSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MeldestelleButton(
|
fun MsButton(
|
||||||
text: String,
|
text: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -25,7 +26,8 @@ fun MeldestelleButton(
|
|||||||
size: ButtonSize = ButtonSize.MEDIUM,
|
size: ButtonSize = ButtonSize.MEDIUM,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
fullWidth: Boolean = false
|
fullWidth: Boolean = false,
|
||||||
|
containerColor: Color? = null
|
||||||
) {
|
) {
|
||||||
val buttonModifier = modifier.then(
|
val buttonModifier = modifier.then(
|
||||||
if (fullWidth) Modifier.fillMaxWidth() else Modifier
|
if (fullWidth) Modifier.fillMaxWidth() else Modifier
|
||||||
@@ -41,7 +43,8 @@ fun MeldestelleButton(
|
|||||||
ButtonVariant.PRIMARY -> Button(
|
ButtonVariant.PRIMARY -> Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading
|
enabled = enabled && !isLoading,
|
||||||
|
colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors()
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading)
|
||||||
}
|
}
|
||||||
@@ -49,7 +52,8 @@ fun MeldestelleButton(
|
|||||||
ButtonVariant.SECONDARY -> FilledTonalButton(
|
ButtonVariant.SECONDARY -> FilledTonalButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading
|
enabled = enabled && !isLoading,
|
||||||
|
colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors()
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading)
|
||||||
}
|
}
|
||||||
@@ -96,7 +100,7 @@ fun PrimaryButton(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
fullWidth: Boolean = false
|
fullWidth: Boolean = false
|
||||||
) = MeldestelleButton(
|
) = MsButton(
|
||||||
text = text,
|
text = text,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -115,7 +119,7 @@ fun SecondaryButton(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
fullWidth: Boolean = false
|
fullWidth: Boolean = false
|
||||||
) = MeldestelleButton(
|
) = MsButton(
|
||||||
text = text,
|
text = text,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
+16
-1
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -20,7 +21,7 @@ import at.mocode.frontend.core.designsystem.theme.Dimens
|
|||||||
* Im Enterprise-Kontext sind flache Cards mit dünnem Border (1px) oft sauberer.
|
* Im Enterprise-Kontext sind flache Cards mit dünnem Border (1px) oft sauberer.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardCard(
|
fun MsCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -40,3 +41,17 @@ fun DashboardCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preview für IDE (muss in jvmMain liegen um in IDEA gerendert zu werden,
|
||||||
|
// oder hier bleiben als Dokumentation)
|
||||||
|
@Composable
|
||||||
|
fun MsCardPreviewContent() {
|
||||||
|
MaterialTheme {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
MsCard {
|
||||||
|
Text("Dies ist eine MsCard", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Text("Mit High-Density Content.", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition einer Spalte für die [MsDataTable].
|
||||||
|
*
|
||||||
|
* @param title Die Beschriftung im Header.
|
||||||
|
* @param width Die Breite der Spalte.
|
||||||
|
* @param weight Wenn gesetzt, dehnt sich die Spalte flexibel aus.
|
||||||
|
* @param alignment Ausrichtung des Inhalts (Start, Center, End).
|
||||||
|
* @param cellRenderer Eigener Renderer für den Inhalt der Zelle.
|
||||||
|
*/
|
||||||
|
data class MsColumnDefinition<T>(
|
||||||
|
val title: String,
|
||||||
|
val width: Dp? = null,
|
||||||
|
val weight: Float? = null,
|
||||||
|
val alignment: Alignment = Alignment.CenterStart,
|
||||||
|
val cellRenderer: @Composable (T) -> Unit = { item ->
|
||||||
|
Text(
|
||||||
|
text = item.toString(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine performante, hochdichte Datentabelle für Desktop-Anwendungen.
|
||||||
|
*
|
||||||
|
* Warum?
|
||||||
|
* Standard-Material-Tabellen sind oft zu großzügig mit Padding.
|
||||||
|
* In der Meldestelle müssen wir viele Daten auf einen Blick sehen.
|
||||||
|
*
|
||||||
|
* @param items Die anzuzeigenden Datenobjekte.
|
||||||
|
* @param columns Die Definitionen der Spalten.
|
||||||
|
* @param onRowClick Callback, wenn eine Zeile angeklickt wird.
|
||||||
|
* @param modifier Der Modifier für die gesamte Tabelle.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> MsDataTable(
|
||||||
|
items: List<T>,
|
||||||
|
columns: List<MsColumnDefinition<T>>,
|
||||||
|
onRowClick: ((T) -> Unit)? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
alternateRowBackgroundColor: Color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// --- 1. Header (Sticky) ---
|
||||||
|
Surface(
|
||||||
|
color = headerBackgroundColor,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = Dimens.SpacingXS),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
columns.forEach { col ->
|
||||||
|
val colModifier = when {
|
||||||
|
col.weight != null -> Modifier.weight(col.weight)
|
||||||
|
col.width != null -> Modifier.width(col.width)
|
||||||
|
else -> Modifier.wrapContentWidth()
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = colModifier,
|
||||||
|
contentAlignment = col.alignment
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = col.title.uppercase(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Body (LazyColumn) ---
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
itemsIndexed(items) { index, item ->
|
||||||
|
val bgColor = if (index % 2 == 0) rowBackgroundColor else alternateRowBackgroundColor
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = bgColor,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = onRowClick != null) { onRowClick?.invoke(item) }
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = 6.dp), // Kompakte Zeilenhöhe
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
columns.forEach { col ->
|
||||||
|
val colModifier = when {
|
||||||
|
col.weight != null -> Modifier.weight(col.weight)
|
||||||
|
col.width != null -> Modifier.width(col.width)
|
||||||
|
else -> Modifier.wrapContentWidth()
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = colModifier,
|
||||||
|
contentAlignment = col.alignment
|
||||||
|
) {
|
||||||
|
col.cellRenderer(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f), thickness = 0.5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einheitlicher Rahmen für modale Dialoge.
|
||||||
|
*
|
||||||
|
* @param title Die Überschrift des Dialogs.
|
||||||
|
* @param onDismissRequest Callback, wenn der Dialog geschlossen werden soll (z.B. Klick außerhalb).
|
||||||
|
* @param confirmButton Die primäre Aktion (z.B. OK, Speichern).
|
||||||
|
* @param dismissButton Die sekundäre Aktion (z.B. Abbrechen).
|
||||||
|
* @param modifier Modifier für das Surface des Dialogs.
|
||||||
|
* @param content Der eigentliche Inhalt des Dialogs.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsDialogShell(
|
||||||
|
title: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
confirmButton: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
dismissButton: @Composable (() -> Unit)? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onDismissRequest) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
tonalElevation = 6.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(Dimens.SpacingM)
|
||||||
|
) {
|
||||||
|
// --- 1. Titel-Bereich ---
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(bottom = Dimens.SpacingM)
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- 2. Content-Bereich ---
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Dimens.SpacingM))
|
||||||
|
|
||||||
|
// --- 3. Button-Leiste ---
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (dismissButton != null) {
|
||||||
|
dismissButton()
|
||||||
|
Spacer(modifier = Modifier.width(Dimens.SpacingS))
|
||||||
|
}
|
||||||
|
confirmButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfs-Funktion für einen Standard-Bestätigungsdialog.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsConfirmDialog(
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
confirmText: String = "Bestätigen",
|
||||||
|
dismissText: String = "Abbrechen",
|
||||||
|
isDestructive: Boolean = false
|
||||||
|
) {
|
||||||
|
MsDialogShell(
|
||||||
|
title = title,
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
MsButton(
|
||||||
|
text = confirmText,
|
||||||
|
onClick = onConfirm,
|
||||||
|
variant = if (isDestructive) ButtonVariant.PRIMARY else ButtonVariant.PRIMARY,
|
||||||
|
// Bei destruktiven Aktionen könnten wir hier später eine rote Farbe erzwingen
|
||||||
|
containerColor = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
MsButton(
|
||||||
|
text = dismissText,
|
||||||
|
onClick = onDismiss,
|
||||||
|
variant = ButtonVariant.TEXT,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein generisches Dropdown zur Auswahl von Enum-Werten.
|
||||||
|
*
|
||||||
|
* @param label Das Label über dem Dropdown.
|
||||||
|
* @param options Alle verfügbaren Enum-Optionen (z.B. SparteE.values()).
|
||||||
|
* @param selectedOption Der aktuell gewählte Wert.
|
||||||
|
* @param onOptionSelected Callback bei Auswahl einer Option.
|
||||||
|
* @param optionLabel Transformation des Enums in einen lesbaren Text (Standard: toString()).
|
||||||
|
* @param modifier Modifier für die gesamte Komponente.
|
||||||
|
* @param enabled Ob das Dropdown bearbeitbar ist.
|
||||||
|
* @param isError Ob ein Fehler vorliegt.
|
||||||
|
* @param errorMessage Die anzuzeigende Fehlermeldung.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun <T : Enum<T>> MsEnumDropdown(
|
||||||
|
label: String,
|
||||||
|
options: Array<T>,
|
||||||
|
selectedOption: T?,
|
||||||
|
onOptionSelected: (T) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
optionLabel: (T) -> String = { it.name },
|
||||||
|
enabled: Boolean = true,
|
||||||
|
isError: Boolean = false,
|
||||||
|
errorMessage: String? = null
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { if (enabled) expanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedOption?.let { optionLabel(it) } ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, enabled)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
isError = isError,
|
||||||
|
enabled = enabled,
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
options.forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = optionLabel(option),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onOptionSelected(option)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError && errorMessage != null) {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einheitliche Filterzeile für alle Stammdaten-Listen.
|
||||||
|
*
|
||||||
|
* @param searchQuery Der aktuelle Suchbegriff.
|
||||||
|
* @param onSearchQueryChange Callback bei Änderung der Suche.
|
||||||
|
* @param searchPlaceholder Platzhalter im Suchfeld.
|
||||||
|
* @param filters Sektion für Filter-Chips (Optional).
|
||||||
|
* @param actions Sektion für zusätzliche Aktionen am Ende (Optional).
|
||||||
|
* @param resultCount Anzahl der gefundenen Einträge (Optional).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsFilterBar(
|
||||||
|
searchQuery: String,
|
||||||
|
onSearchQueryChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
searchPlaceholder: String = "Suchen...",
|
||||||
|
filters: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
actions: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
resultCount: Int? = null
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = Dimens.SpacingXS),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// --- 1. Suchfeld (Kompakt) ---
|
||||||
|
OutlinedTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onSearchQueryChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(300.dp)
|
||||||
|
.height(40.dp), // Fixe Höhe für High-Density
|
||||||
|
placeholder = { Text(searchPlaceholder, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
||||||
|
trailingIcon = if (searchQuery.isNotEmpty()) {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onSearchQueryChange("") }) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(Dimens.SpacingM))
|
||||||
|
|
||||||
|
// --- 2. Filter-Chips ---
|
||||||
|
if (filters != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.FilterList,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
filters()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. Result Count ---
|
||||||
|
if (resultCount != null) {
|
||||||
|
Text(
|
||||||
|
text = "$resultCount Einträge",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(horizontal = Dimens.SpacingM)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Aktionen ---
|
||||||
|
if (actions != null) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||||
|
) {
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein kompakter Filter-Chip für die [MsFilterBar].
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MsFilterChip(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
leadingIcon: ImageVector? = null
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
label = { Text(label, style = MaterialTheme.typography.labelSmall) },
|
||||||
|
modifier = modifier.height(28.dp), // Kompakte Höhe
|
||||||
|
leadingIcon = leadingIcon?.let {
|
||||||
|
{ Icon(it, contentDescription = null, modifier = Modifier.size(14.dp)) }
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -13,7 +13,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppFooter() {
|
fun MsFooter() {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
+1
-1
@@ -12,7 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppHeader(
|
fun MsHeader(
|
||||||
isAuthenticated: Boolean,
|
isAuthenticated: Boolean,
|
||||||
username: String?,
|
username: String?,
|
||||||
onNavigateToLogin: (() -> Unit)? = null,
|
onNavigateToLogin: (() -> Unit)? = null,
|
||||||
+7
-4
@@ -1,7 +1,10 @@
|
|||||||
package at.mocode.frontend.core.designsystem.components
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -13,7 +16,7 @@ enum class LoadingSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingIndicator(
|
fun MsLoadingIndicator(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
size: LoadingSize = LoadingSize.MEDIUM,
|
size: LoadingSize = LoadingSize.MEDIUM,
|
||||||
message: String? = null
|
message: String? = null
|
||||||
@@ -61,7 +64,7 @@ fun FullScreenLoading(
|
|||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LoadingIndicator(
|
MsLoadingIndicator(
|
||||||
size = LoadingSize.LARGE,
|
size = LoadingSize.LARGE,
|
||||||
message = message
|
message = message
|
||||||
)
|
)
|
||||||
@@ -79,7 +82,7 @@ fun InlineLoading(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
LoadingIndicator(
|
MsLoadingIndicator(
|
||||||
size = LoadingSize.SMALL,
|
size = LoadingSize.SMALL,
|
||||||
message = message
|
message = message
|
||||||
)
|
)
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein standardisiertes Master-Detail Layout für die Stammdaten-Verwaltung.
|
||||||
|
*
|
||||||
|
* @param master Der Bereich auf der linken Seite (z.B. MsDataTable mit MsFilterBar).
|
||||||
|
* @param detail Der Bereich auf der rechten Seite (z.B. ein Editor-Formular).
|
||||||
|
* @param modifier Der Modifier für das gesamte Layout.
|
||||||
|
* @param masterWeight Der Anteil des Master-Bereichs (Standard: 0.4).
|
||||||
|
* @param detailHeader Optionaler Header für den Detail-Bereich (z.B. MsActionToolbar).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsMasterDetailLayout(
|
||||||
|
master: @Composable BoxScope.() -> Unit,
|
||||||
|
detail: @Composable BoxScope.() -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
masterWeight: Float = 0.4f,
|
||||||
|
detailHeader: @Composable (RowScope.() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier.fillMaxSize()) {
|
||||||
|
// --- 1. Master-Bereich (z.B. Liste) ---
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(masterWeight)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(Dimens.SpacingS)
|
||||||
|
) {
|
||||||
|
master()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Trennlinie (Vertikal) ---
|
||||||
|
VerticalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- 3. Detail-Bereich (z.B. Editor) ---
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f - masterWeight)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
// Optionaler Header für Aktionen
|
||||||
|
if (detailHeader != null) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.SpacingM, vertical = Dimens.SpacingS),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
detailHeader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(Dimens.SpacingM)
|
||||||
|
) {
|
||||||
|
detail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -10,13 +10,13 @@ import androidx.compose.ui.Modifier
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScaffold(
|
fun MsScaffold(
|
||||||
header: @Composable () -> Unit = {
|
header: @Composable () -> Unit = {
|
||||||
AppHeader(isAuthenticated = false, username = null)
|
MsHeader(isAuthenticated = false, username = null)
|
||||||
},
|
},
|
||||||
content: @Composable (PaddingValues) -> Unit,
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
footer: @Composable () -> Unit = {
|
footer: @Composable () -> Unit = {
|
||||||
AppFooter()
|
MsFooter()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
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.Search
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Komponente zur Suche und Auswahl von Objekten aus einer Liste.
|
||||||
|
* Ideal für die Auswahl von Reitern, Pferden oder Vereinen.
|
||||||
|
*
|
||||||
|
* @param label Das Label über dem Auswahlfeld.
|
||||||
|
* @param selectedOption Das aktuell gewählte Objekt.
|
||||||
|
* @param onOptionSelected Callback bei Auswahl eines Objekts.
|
||||||
|
* @param onSearchQueryChange Callback bei Änderung des Suchbegriffs (für API-Calls).
|
||||||
|
* @param options Die aktuell verfügbaren Optionen (Suchergebnisse).
|
||||||
|
* @param optionLabel Transformation des Objekts in einen lesbaren Text.
|
||||||
|
* @param modifier Modifier für die gesamte Komponente.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> MsSearchableSelect(
|
||||||
|
label: String,
|
||||||
|
selectedOption: T?,
|
||||||
|
onOptionSelected: (T) -> Unit,
|
||||||
|
onSearchQueryChange: (String) -> Unit,
|
||||||
|
options: List<T>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
optionLabel: (T) -> String = { it.toString() },
|
||||||
|
enabled: Boolean = true,
|
||||||
|
placeholder: String = "Suchen & Auswählen..."
|
||||||
|
) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var searchText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedOption?.let { optionLabel(it) } ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = enabled) { showDialog = true },
|
||||||
|
enabled = enabled,
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
|
||||||
|
if (showDialog) {
|
||||||
|
Dialog(onDismissRequest = { showDialog = false }) {
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
tonalElevation = 8.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.8f)
|
||||||
|
.fillMaxHeight(0.7f)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Internes Suchfeld im Dialog
|
||||||
|
OutlinedTextField(
|
||||||
|
value = searchText,
|
||||||
|
onValueChange = {
|
||||||
|
searchText = it
|
||||||
|
onSearchQueryChange(it)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
placeholder = { Text("Suchbegriff eingeben...") },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
|
singleLine = true,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Ergebnisliste
|
||||||
|
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||||
|
items(options) { option ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = optionLabel(option),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onOptionSelected(option)
|
||||||
|
showDialog = false
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { showDialog = false }) {
|
||||||
|
Text("Abbrechen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein kompakter Badge zur Anzeige von Status-Informationen.
|
||||||
|
*
|
||||||
|
* @param text Der anzuzeigende Text.
|
||||||
|
* @param containerColor Die Hintergrundfarbe des Badges.
|
||||||
|
* @param contentColor Die Textfarbe des Badges.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsStatusBadge(
|
||||||
|
text: String,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.background(color = containerColor, shape = RoundedCornerShape(4.dp))
|
||||||
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text.uppercase(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vordefinierte Status-Typen für eine konsistente UX.
|
||||||
|
*/
|
||||||
|
object MsStatusDefaults {
|
||||||
|
@Composable
|
||||||
|
fun Success(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFE8F5E9),
|
||||||
|
contentColor = Color(0xFF2E7D32),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Warning(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFFFF3E0),
|
||||||
|
contentColor = Color(0xFFEF6C00),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Error(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFFFEBEE),
|
||||||
|
contentColor = Color(0xFFC62828),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Info(text: String, modifier: Modifier = Modifier) = MsStatusBadge(
|
||||||
|
text = text,
|
||||||
|
containerColor = Color(0xFFE3F2FD),
|
||||||
|
contentColor = Color(0xFF1565C0),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-3
@@ -16,7 +16,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MeldestelleTextField(
|
fun MsTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -109,7 +109,7 @@ fun MeldestellePasswordField(
|
|||||||
) {
|
) {
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
MeldestelleTextField(
|
MsTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -152,7 +152,7 @@ fun MeldestelleEmailField(
|
|||||||
imeAction: ImeAction = ImeAction.Next,
|
imeAction: ImeAction = ImeAction.Next,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default
|
keyboardActions: KeyboardActions = KeyboardActions.Default
|
||||||
) {
|
) {
|
||||||
MeldestelleTextField(
|
MsTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ErrorOutline
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.WarningAmber
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Schwere einer Validierungsmeldung.
|
||||||
|
*/
|
||||||
|
enum class ValidationSeverity {
|
||||||
|
ERROR, // Blokierend (z.B. fehlende Pflichtfelder)
|
||||||
|
WARNING, // Hinweisend (z.B. § 39 ÖTO - Abteilungstrennung steht bevor)
|
||||||
|
INFO // Informativ (z.B. Reiter hat heute Geburtstag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einzelne Validierungsmeldung.
|
||||||
|
*/
|
||||||
|
data class ValidationMessage(
|
||||||
|
val message: String,
|
||||||
|
val severity: ValidationSeverity = ValidationSeverity.ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Wrapper für Eingabekomponenten, um Validierungsergebnisse (ÖTO-Regeln) anzuzeigen.
|
||||||
|
*
|
||||||
|
* @param messages Liste der anzuzeigenden Meldungen.
|
||||||
|
* @param modifier Der Modifier für den äußeren Container.
|
||||||
|
* @param content Die Eingabekomponente (z.B. MsTextField, MsEnumDropdown).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsValidationWrapper(
|
||||||
|
messages: List<ValidationMessage>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// Die eigentliche Eingabekomponente
|
||||||
|
content()
|
||||||
|
|
||||||
|
// Validierungsmeldungen unterhalb
|
||||||
|
if (messages.isNotEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp, start = 12.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
messages.forEach { msg ->
|
||||||
|
ValidationRow(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine einzelne Zeile für eine Validierungsmeldung mit passendem Icon und Farbe.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ValidationRow(msg: ValidationMessage) {
|
||||||
|
val (color, icon) = when (msg.severity) {
|
||||||
|
ValidationSeverity.ERROR -> MaterialTheme.colorScheme.error to Icons.Default.ErrorOutline
|
||||||
|
ValidationSeverity.WARNING -> Color(0xFFEF6C00) to Icons.Default.WarningAmber // Warmer Orange-Ton
|
||||||
|
ValidationSeverity.INFO -> MaterialTheme.colorScheme.primary to Icons.Default.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = color
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = msg.message,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -24,8 +24,10 @@ sealed class AppScreen(val route: String) {
|
|||||||
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
||||||
data object VeranstalterNeu : AppScreen("/veranstalter/neu")
|
data object VeranstalterNeu : AppScreen("/veranstalter/neu")
|
||||||
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||||
// Neue Veranstaltungs-Konfig-Seite (aus Veranstalter-Detail → "+ Neue Veranstaltung")
|
|
||||||
data class VeranstaltungKonfig(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId/veranstaltung/neu")
|
// 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 VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/veranstaltung/$veranstaltungId/turnier/neu")
|
data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/veranstaltung/$veranstaltungId/turnier/neu")
|
||||||
data object Reiter : AppScreen("/reiter")
|
data object Reiter : AppScreen("/reiter")
|
||||||
data object Pferde : AppScreen("/pferde")
|
data object Pferde : AppScreen("/pferde")
|
||||||
|
data object Vereine : AppScreen("/vereine")
|
||||||
data object Funktionaere : AppScreen("/funktionaere")
|
data object Funktionaere : AppScreen("/funktionaere")
|
||||||
data object Meisterschaften : AppScreen("/meisterschaften")
|
data object Meisterschaften : AppScreen("/meisterschaften")
|
||||||
data object Cups : AppScreen("/cups")
|
data object Cups : AppScreen("/cups")
|
||||||
@@ -68,6 +71,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
"/veranstaltung/neu" -> VeranstaltungNeu
|
"/veranstaltung/neu" -> VeranstaltungNeu
|
||||||
"/reiter" -> Reiter
|
"/reiter" -> Reiter
|
||||||
"/pferde" -> Pferde
|
"/pferde" -> Pferde
|
||||||
|
"/vereine" -> Vereine
|
||||||
"/funktionaere" -> Funktionaere
|
"/funktionaere" -> Funktionaere
|
||||||
"/meisterschaften" -> Meisterschaften
|
"/meisterschaften" -> Meisterschaften
|
||||||
"/cups" -> Cups
|
"/cups" -> Cups
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Feature-Modul: Pferde-Verwaltung (Desktop-only)
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "at.mocode.clients"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
sourceSets {
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(projects.frontend.core.designSystem)
|
||||||
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(projects.frontend.core.navigation)
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
|
implementation(libs.bundles.kmp.common)
|
||||||
|
implementation(libs.koin.core)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package at.mocode.frontend.features.pferde.domain
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-Modell für ein Pferd.
|
||||||
|
*/
|
||||||
|
data class Pferd(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val lebensnummer: String,
|
||||||
|
val geschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||||
|
val farbe: String = "",
|
||||||
|
val geburtsjahr: Int? = null,
|
||||||
|
val status: PferdeStatus = PferdeStatus.AKTIV
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class Geschlecht(val label: String) {
|
||||||
|
WALLACH("Wallach"),
|
||||||
|
STUTE("Stute"),
|
||||||
|
HENGST("Hengst")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PferdeStatus(val label: String, val color: Color) {
|
||||||
|
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||||
|
INAKTIV("Inaktiv", Color(0xFF757575)),
|
||||||
|
GESTOKEN("Gestorben", Color(0xFFC62828)),
|
||||||
|
VERKAUFT("Verkauft", Color(0xFF0277BD))
|
||||||
|
}
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
package at.mocode.frontend.features.pferde.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
import at.mocode.frontend.features.pferde.domain.Geschlecht
|
||||||
|
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||||
|
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PferdeScreen(
|
||||||
|
viewModel: PferdeViewModel = PferdeViewModel()
|
||||||
|
) {
|
||||||
|
val uiState = viewModel.uiState
|
||||||
|
|
||||||
|
MsMasterDetailLayout(
|
||||||
|
master = {
|
||||||
|
PferdeListContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
|
onPferdSelected = viewModel::selectPferd
|
||||||
|
)
|
||||||
|
},
|
||||||
|
detail = {
|
||||||
|
if (uiState.isEditing) {
|
||||||
|
PferdeEditorContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onNameChange = viewModel::onEditNameChange,
|
||||||
|
onLebensnummerChange = viewModel::onEditLebensnummerChange,
|
||||||
|
onGeschlechtChange = viewModel::onEditGeschlechtChange,
|
||||||
|
onFarbeChange = viewModel::onEditFarbeChange,
|
||||||
|
onGeburtsjahrChange = viewModel::onEditGeburtsjahrChange,
|
||||||
|
onStatusChange = viewModel::onEditStatusChange,
|
||||||
|
onSave = viewModel::onSave,
|
||||||
|
onCancel = viewModel::onCancel
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PlaceholderContent(
|
||||||
|
title = "Kein Pferd ausgewählt",
|
||||||
|
subtitle = "Wählen Sie ein Pferd aus der Liste aus oder legen Sie ein neues an."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PferdeListContent(
|
||||||
|
uiState: PferdeUiState,
|
||||||
|
onSearchChange: (String) -> Unit,
|
||||||
|
onPferdSelected: (Pferd) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsFilterBar(
|
||||||
|
searchQuery = uiState.searchQuery,
|
||||||
|
onSearchQueryChange = onSearchChange,
|
||||||
|
resultCount = uiState.searchResults.size
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
MsDataTable(
|
||||||
|
items = uiState.searchResults,
|
||||||
|
columns = listOf(
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Name",
|
||||||
|
weight = 1f,
|
||||||
|
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Lebensnummer",
|
||||||
|
width = 150.dp,
|
||||||
|
cellRenderer = { Text(it.lebensnummer, style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Status",
|
||||||
|
width = 100.dp,
|
||||||
|
cellRenderer = {
|
||||||
|
MsStatusBadge(
|
||||||
|
text = it.status.label,
|
||||||
|
containerColor = it.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = it.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onRowClick = onPferdSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PferdeEditorContent(
|
||||||
|
uiState: PferdeUiState,
|
||||||
|
onNameChange: (String) -> Unit,
|
||||||
|
onLebensnummerChange: (String) -> Unit,
|
||||||
|
onGeschlechtChange: (Geschlecht) -> Unit,
|
||||||
|
onFarbeChange: (String) -> Unit,
|
||||||
|
onGeburtsjahrChange: (String) -> Unit,
|
||||||
|
onStatusChange: (PferdeStatus) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsActionToolbar(
|
||||||
|
title = "Pferde Details",
|
||||||
|
onSave = onSave,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editName,
|
||||||
|
onValueChange = onNameChange,
|
||||||
|
label = "Name",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editLebensnummer,
|
||||||
|
onValueChange = onLebensnummerChange,
|
||||||
|
label = "Lebensnummer",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsEnumDropdown(
|
||||||
|
label = "Geschlecht",
|
||||||
|
options = Geschlecht.entries.toTypedArray(),
|
||||||
|
selectedOption = uiState.editGeschlecht,
|
||||||
|
onOptionSelected = onGeschlechtChange,
|
||||||
|
optionLabel = { it.label },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editFarbe,
|
||||||
|
onValueChange = onFarbeChange,
|
||||||
|
label = "Farbe",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editGeburtsjahr,
|
||||||
|
onValueChange = onGeburtsjahrChange,
|
||||||
|
label = "Geburtsjahr",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MsEnumDropdown(
|
||||||
|
label = "Status",
|
||||||
|
options = PferdeStatus.entries.toTypedArray(),
|
||||||
|
selectedOption = uiState.editStatus,
|
||||||
|
onOptionSelected = onStatusChange,
|
||||||
|
optionLabel = { it.label },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
if (uiState.editStatus == PferdeStatus.INAKTIV) {
|
||||||
|
MsValidationWrapper(
|
||||||
|
messages = listOf(
|
||||||
|
ValidationMessage(
|
||||||
|
"Pferd ist als inaktiv markiert und kann nicht für Nennungen verwendet werden.",
|
||||||
|
ValidationSeverity.WARNING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Zusätzliche Pferde-Informationen",
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-Place Preview für den PferdeScreen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PferdeScreenPreviewContent() {
|
||||||
|
val viewModel = PferdeViewModel()
|
||||||
|
at.mocode.frontend.core.designsystem.theme.AppTheme {
|
||||||
|
Surface {
|
||||||
|
PferdeScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package at.mocode.frontend.features.pferde.presentation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import at.mocode.frontend.features.pferde.domain.Geschlecht
|
||||||
|
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||||
|
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-State für die Pferde-Verwaltung.
|
||||||
|
*/
|
||||||
|
data class PferdeUiState(
|
||||||
|
val searchResults: List<Pferd> = emptyList(),
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val selectedPferd: Pferd? = null,
|
||||||
|
val isEditing: Boolean = false,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val editName: String = "",
|
||||||
|
val editLebensnummer: String = "",
|
||||||
|
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||||
|
val editFarbe: String = "",
|
||||||
|
val editGeburtsjahr: String = "",
|
||||||
|
val editStatus: PferdeStatus = PferdeStatus.AKTIV
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel für die Pferde-Verwaltung.
|
||||||
|
*/
|
||||||
|
open class PferdeViewModel(initialLoad: Boolean = true) {
|
||||||
|
var uiState by mutableStateOf(PferdeUiState())
|
||||||
|
protected set
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (initialLoad) {
|
||||||
|
loadPferde()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPferde() {
|
||||||
|
val mockData = listOf(
|
||||||
|
Pferd("1", "Bella", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
||||||
|
Pferd("2", "Casanova", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
||||||
|
Pferd("3", "Spirit", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
||||||
|
Pferd("4", "Lucky", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
||||||
|
)
|
||||||
|
uiState = uiState.copy(searchResults = mockData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchQueryChange(query: String) {
|
||||||
|
uiState = uiState.copy(searchQuery = query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectPferd(pferd: Pferd) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedPferd = pferd,
|
||||||
|
isEditing = true,
|
||||||
|
editName = pferd.name,
|
||||||
|
editLebensnummer = pferd.lebensnummer,
|
||||||
|
editGeschlecht = pferd.geschlecht,
|
||||||
|
editFarbe = pferd.farbe,
|
||||||
|
editGeburtsjahr = pferd.geburtsjahr?.toString() ?: "",
|
||||||
|
editStatus = pferd.status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditNameChange(value: String) {
|
||||||
|
uiState = uiState.copy(editName = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditLebensnummerChange(value: String) {
|
||||||
|
uiState = uiState.copy(editLebensnummer = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditGeschlechtChange(value: Geschlecht) {
|
||||||
|
uiState = uiState.copy(editGeschlecht = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditFarbeChange(value: String) {
|
||||||
|
uiState = uiState.copy(editFarbe = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditGeburtsjahrChange(value: String) {
|
||||||
|
uiState = uiState.copy(editGeburtsjahr = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditStatusChange(value: PferdeStatus) {
|
||||||
|
uiState = uiState.copy(editStatus = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSave() {
|
||||||
|
uiState = uiState.copy(isEditing = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCancel() {
|
||||||
|
uiState = uiState.copy(isEditing = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-13
@@ -15,8 +15,9 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
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 at.mocode.frontend.core.designsystem.components.DashboardCard
|
import at.mocode.frontend.core.designsystem.components.ButtonSize
|
||||||
import at.mocode.frontend.core.designsystem.components.DenseButton
|
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsCard
|
||||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -58,7 +59,7 @@ fun PingScreen(
|
|||||||
|
|
||||||
// Right Panel: Terminal Log (40%)
|
// Right Panel: Terminal Log (40%)
|
||||||
// Hier nutzen wir bewusst einen dunklen "Terminal"-Look, unabhängig vom Theme
|
// Hier nutzen wir bewusst einen dunklen "Terminal"-Look, unabhängig vom Theme
|
||||||
DashboardCard(
|
MsCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(0.4f)
|
.weight(0.4f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
@@ -139,14 +140,14 @@ private fun ActionToolbar(viewModel: PingViewModel) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS),
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS),
|
||||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||||
) {
|
) {
|
||||||
DenseButton(text = "Simple", onClick = { viewModel.performSimplePing() })
|
MsButton(text = "Simple", size = ButtonSize.SMALL, onClick = { viewModel.performSimplePing() })
|
||||||
DenseButton(text = "Enhanced", onClick = { viewModel.performEnhancedPing() })
|
MsButton(text = "Enhanced", size = ButtonSize.SMALL, onClick = { viewModel.performEnhancedPing() })
|
||||||
DenseButton(text = "Secure", onClick = { viewModel.performSecurePing() })
|
MsButton(text = "Secure", size = ButtonSize.SMALL, onClick = { viewModel.performSecurePing() })
|
||||||
DenseButton(text = "Health", onClick = { viewModel.performHealthCheck() })
|
MsButton(text = "Health", size = ButtonSize.SMALL, onClick = { viewModel.performHealthCheck() })
|
||||||
DenseButton(
|
MsButton(
|
||||||
text = "Sync",
|
text = "Sync",
|
||||||
onClick = { viewModel.triggerSync() },
|
size = ButtonSize.SMALL,
|
||||||
containerColor = MaterialTheme.colorScheme.secondary
|
onClick = { viewModel.triggerSync() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +157,7 @@ private fun StatusGrid(uiState: PingUiState) {
|
|||||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||||
// Row 1
|
// Row 1
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||||
DashboardCard(modifier = Modifier.weight(1f)) {
|
MsCard(modifier = Modifier.weight(1f)) {
|
||||||
StatusHeader("SIMPLE / SECURE PING")
|
StatusHeader("SIMPLE / SECURE PING")
|
||||||
if (uiState.simplePingResponse != null) {
|
if (uiState.simplePingResponse != null) {
|
||||||
KeyValueRow("Status", uiState.simplePingResponse.status)
|
KeyValueRow("Status", uiState.simplePingResponse.status)
|
||||||
@@ -167,7 +168,7 @@ private fun StatusGrid(uiState: PingUiState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DashboardCard(modifier = Modifier.weight(1f)) {
|
MsCard(modifier = Modifier.weight(1f)) {
|
||||||
StatusHeader("HEALTH CHECK")
|
StatusHeader("HEALTH CHECK")
|
||||||
if (uiState.healthResponse != null) {
|
if (uiState.healthResponse != null) {
|
||||||
KeyValueRow("Status", uiState.healthResponse.status)
|
KeyValueRow("Status", uiState.healthResponse.status)
|
||||||
@@ -180,7 +181,7 @@ private fun StatusGrid(uiState: PingUiState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Row 2
|
// Row 2
|
||||||
DashboardCard(modifier = Modifier.fillMaxWidth()) {
|
MsCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
StatusHeader("ENHANCED PING (RESILIENCE)")
|
StatusHeader("ENHANCED PING (RESILIENCE)")
|
||||||
if (uiState.enhancedPingResponse != null) {
|
if (uiState.enhancedPingResponse != null) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
|||||||
+8
-8
@@ -13,9 +13,9 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.LoadingIndicator
|
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||||
import at.mocode.frontend.core.designsystem.components.MeldestelleButton
|
import at.mocode.frontend.core.designsystem.components.MsLoadingIndicator
|
||||||
import at.mocode.frontend.core.designsystem.components.MeldestelleTextField
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
@@ -37,7 +37,7 @@ fun ProfileScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
LoadingIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
|
MsLoadingIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||||
} else {
|
} else {
|
||||||
// Fehleranzeige
|
// Fehleranzeige
|
||||||
uiState.errorMessage?.let { error ->
|
uiState.errorMessage?.let { error ->
|
||||||
@@ -100,7 +100,7 @@ fun ZnsLinkSection(
|
|||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
MeldestelleTextField(
|
MsTextField(
|
||||||
value = satznummer,
|
value = satznummer,
|
||||||
onValueChange = { satznummer = it },
|
onValueChange = { satznummer = it },
|
||||||
label = "ZNS Satznummer",
|
label = "ZNS Satznummer",
|
||||||
@@ -112,7 +112,7 @@ fun ZnsLinkSection(
|
|||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
MeldestelleButton(
|
MsButton(
|
||||||
onClick = { onLink(satznummer) },
|
onClick = { onLink(satznummer) },
|
||||||
text = "Jetzt verknüpfen",
|
text = "Jetzt verknüpfen",
|
||||||
isLoading = isLinking,
|
isLoading = isLinking,
|
||||||
@@ -154,7 +154,7 @@ fun ProfileDetailsSection(
|
|||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
MeldestelleTextField(
|
MsTextField(
|
||||||
value = contactEmail,
|
value = contactEmail,
|
||||||
onValueChange = { contactEmail = it },
|
onValueChange = { contactEmail = it },
|
||||||
label = "Kontakt E-Mail",
|
label = "Kontakt E-Mail",
|
||||||
@@ -162,7 +162,7 @@ fun ProfileDetailsSection(
|
|||||||
leadingIcon = Icons.Default.Email
|
leadingIcon = Icons.Default.Email
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
MeldestelleTextField(
|
MsTextField(
|
||||||
value = bio,
|
value = bio,
|
||||||
onValueChange = { bio = it },
|
onValueChange = { bio = it },
|
||||||
label = "Info / Bio",
|
label = "Info / Bio",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Feature-Modul: Reiter-Verwaltung (Desktop-only)
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
}
|
||||||
|
group = "at.mocode.clients"
|
||||||
|
version = "1.0.0"
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
sourceSets {
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(projects.frontend.core.designSystem)
|
||||||
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(projects.frontend.core.navigation)
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
|
implementation(libs.bundles.kmp.common)
|
||||||
|
implementation(libs.koin.core)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package at.mocode.frontend.features.reiter.domain
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-Modell für einen Reiter.
|
||||||
|
*/
|
||||||
|
data class Reiter(
|
||||||
|
val id: String,
|
||||||
|
val vorname: String,
|
||||||
|
val nachname: String,
|
||||||
|
val satznummer: String?,
|
||||||
|
val lizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||||
|
val sparte: Sparte = Sparte.KEINE,
|
||||||
|
val status: ReiterStatus = ReiterStatus.AKTIV
|
||||||
|
) {
|
||||||
|
val name: String get() = "$vorname $nachname"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class LizenzKlasse(val label: String) {
|
||||||
|
KEINE("-"),
|
||||||
|
R1("R1"),
|
||||||
|
R1D1("R1D1"),
|
||||||
|
R1S1("R1S1"),
|
||||||
|
R2("R2"),
|
||||||
|
R2D2("R2D2"),
|
||||||
|
R2S2("R2S2"),
|
||||||
|
R3("R3"),
|
||||||
|
R4("R4")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Sparte(val label: String) {
|
||||||
|
KEINE("-"),
|
||||||
|
DRESSUR("Dressur"),
|
||||||
|
SPRINGEN("Springen"),
|
||||||
|
VIELSEITIGKEIT("Vielseitigkeit"),
|
||||||
|
VOLTIGIEREN("Voltigieren"),
|
||||||
|
FAHREN("Fahren"),
|
||||||
|
REINING("Reining")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ReiterStatus(val label: String, val color: Color) {
|
||||||
|
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||||
|
GESPERRT("Gesperrt", Color(0xFFC62828)),
|
||||||
|
INAKTIV("Inaktiv", Color(0xFF757575))
|
||||||
|
}
|
||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
package at.mocode.frontend.features.reiter.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Sparte
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReiterScreen(
|
||||||
|
viewModel: ReiterViewModel
|
||||||
|
) {
|
||||||
|
val uiState = viewModel.uiState
|
||||||
|
|
||||||
|
MsMasterDetailLayout(
|
||||||
|
master = {
|
||||||
|
ReiterListContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
|
onReiterSelected = viewModel::selectReiter
|
||||||
|
)
|
||||||
|
},
|
||||||
|
detail = {
|
||||||
|
if (uiState.isEditing) {
|
||||||
|
ReiterEditorContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onVornameChange = viewModel::onEditVornameChange,
|
||||||
|
onNachnameChange = viewModel::onEditNameChange,
|
||||||
|
onLizenzChange = viewModel::onEditLizenzChange,
|
||||||
|
onSparteChange = viewModel::onEditSparteChange,
|
||||||
|
onSave = viewModel::onSave,
|
||||||
|
onCancel = viewModel::onCancel
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PlaceholderContent(
|
||||||
|
title = "Kein Reiter ausgewählt",
|
||||||
|
subtitle = "Wählen Sie einen Reiter aus der Liste aus oder legen Sie einen neuen an."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReiterListContent(
|
||||||
|
uiState: ReiterUiState,
|
||||||
|
onSearchChange: (String) -> Unit,
|
||||||
|
onReiterSelected: (Reiter) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsFilterBar(
|
||||||
|
searchQuery = uiState.searchQuery,
|
||||||
|
onSearchQueryChange = onSearchChange,
|
||||||
|
resultCount = uiState.searchResults.size
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
MsDataTable(
|
||||||
|
items = uiState.searchResults,
|
||||||
|
columns = listOf(
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Vorname",
|
||||||
|
weight = 1f,
|
||||||
|
cellRenderer = { Text(it.vorname, style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Nachname",
|
||||||
|
weight = 1f,
|
||||||
|
cellRenderer = { Text(it.nachname, style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Lizenz",
|
||||||
|
width = 80.dp,
|
||||||
|
cellRenderer = { Text(it.lizenz.label, style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Status",
|
||||||
|
width = 100.dp,
|
||||||
|
cellRenderer = {
|
||||||
|
MsStatusBadge(
|
||||||
|
text = it.status.label,
|
||||||
|
containerColor = it.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = it.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onRowClick = onReiterSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReiterEditorContent(
|
||||||
|
uiState: ReiterUiState,
|
||||||
|
onVornameChange: (String) -> Unit,
|
||||||
|
onNachnameChange: (String) -> Unit,
|
||||||
|
onLizenzChange: (LizenzKlasse) -> Unit,
|
||||||
|
onSparteChange: (Sparte) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsActionToolbar(
|
||||||
|
title = "Reiter Details",
|
||||||
|
onSave = onSave,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editVorname,
|
||||||
|
onValueChange = onVornameChange,
|
||||||
|
label = "Vorname",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editName,
|
||||||
|
onValueChange = onNachnameChange,
|
||||||
|
label = "Nachname",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsEnumDropdown(
|
||||||
|
label = "Lizenzklasse",
|
||||||
|
options = LizenzKlasse.entries.toTypedArray(),
|
||||||
|
selectedOption = uiState.editLizenz,
|
||||||
|
onOptionSelected = onLizenzChange,
|
||||||
|
optionLabel = { it.label },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MsEnumDropdown(
|
||||||
|
label = "Hauptsparte",
|
||||||
|
options = Sparte.entries.toTypedArray(),
|
||||||
|
selectedOption = uiState.editSparte,
|
||||||
|
onOptionSelected = onSparteChange,
|
||||||
|
optionLabel = { it.label },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Beispiel für ValidationWrapper
|
||||||
|
MsValidationWrapper(
|
||||||
|
messages = listOf(
|
||||||
|
ValidationMessage("Warnung: Lizenz läuft in 14 Tagen ab.", ValidationSeverity.WARNING)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Zusätzliche Reiter-Informationen",
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReiterScreenPreviewContent() {
|
||||||
|
val viewModel = ReiterViewModel().apply {
|
||||||
|
// Optional: Hier könnten Mock-Daten direkt gesetzt werden,
|
||||||
|
// falls das ViewModel dies unterstützt.
|
||||||
|
}
|
||||||
|
MaterialTheme {
|
||||||
|
ReiterScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
package at.mocode.frontend.features.reiter.presentation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||||
|
import at.mocode.frontend.features.reiter.domain.ReiterStatus
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Sparte
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-State für die Reiter-Verwaltung.
|
||||||
|
*/
|
||||||
|
data class ReiterUiState(
|
||||||
|
val searchResults: List<Reiter> = emptyList(),
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val selectedReiter: Reiter? = null,
|
||||||
|
val isEditing: Boolean = false,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val editName: String = "",
|
||||||
|
val editVorname: String = "",
|
||||||
|
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||||
|
val editSparte: Sparte = Sparte.KEINE,
|
||||||
|
val editStatus: ReiterStatus = ReiterStatus.AKTIV
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel für die Reiter-Verwaltung.
|
||||||
|
* In einem echten Szenario würden wir hier ein Repository injizieren.
|
||||||
|
*/
|
||||||
|
open class ReiterViewModel(initialLoad: Boolean = true) {
|
||||||
|
var uiState by mutableStateOf(ReiterUiState())
|
||||||
|
protected set
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (initialLoad) {
|
||||||
|
// Initialer Load (Mock-Daten)
|
||||||
|
loadReiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadReiter() {
|
||||||
|
val mockData = listOf(
|
||||||
|
Reiter("1", "Stefan", "Möbius", "123456", LizenzKlasse.R2D2, Sparte.DRESSUR, ReiterStatus.AKTIV),
|
||||||
|
Reiter("2", "Julia", "Reiterin", "654321", LizenzKlasse.R1, Sparte.SPRINGEN, ReiterStatus.AKTIV),
|
||||||
|
Reiter("3", "Max", "Mustermann", "112233", LizenzKlasse.KEINE, Sparte.KEINE, ReiterStatus.GESPERRT),
|
||||||
|
Reiter("4", "Lisa", "Springen", "445566", LizenzKlasse.R3, Sparte.SPRINGEN, ReiterStatus.AKTIV)
|
||||||
|
)
|
||||||
|
uiState = uiState.copy(searchResults = mockData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchQueryChange(query: String) {
|
||||||
|
uiState = uiState.copy(searchQuery = query)
|
||||||
|
// Hier würde die Filter-Logik greifen
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectReiter(reiter: Reiter) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedReiter = reiter,
|
||||||
|
isEditing = true,
|
||||||
|
editVorname = reiter.vorname,
|
||||||
|
editName = reiter.nachname,
|
||||||
|
editLizenz = reiter.lizenz,
|
||||||
|
editSparte = reiter.sparte,
|
||||||
|
editStatus = reiter.status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditVornameChange(value: String) {
|
||||||
|
uiState = uiState.copy(editVorname = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditNameChange(value: String) {
|
||||||
|
uiState = uiState.copy(editName = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditLizenzChange(value: LizenzKlasse) {
|
||||||
|
uiState = uiState.copy(editLizenz = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditSparteChange(value: Sparte) {
|
||||||
|
uiState = uiState.copy(editSparte = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSave() {
|
||||||
|
// Mock-Speichern
|
||||||
|
uiState = uiState.copy(isEditing = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCancel() {
|
||||||
|
uiState = uiState.copy(isEditing = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package at.mocode.frontend.features.reiter.presentation
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||||
|
import at.mocode.frontend.features.reiter.domain.ReiterStatus
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Sparte
|
||||||
|
import at.mocode.wui.preview.ComponentPreview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilf's-ViewModel für die Vorschau, um den Status direkt setzen zu können.
|
||||||
|
*/
|
||||||
|
private class PreviewReiterViewModel(initialState: ReiterUiState) : ReiterViewModel(initialLoad = false) {
|
||||||
|
init {
|
||||||
|
uiState = initialState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentPreview
|
||||||
|
@Composable
|
||||||
|
fun PreviewReiterScreen_List() {
|
||||||
|
val viewModel = ReiterViewModel() // Nutzt die Mock-Daten aus dem init-Block
|
||||||
|
MaterialTheme {
|
||||||
|
ReiterScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentPreview
|
||||||
|
@Composable
|
||||||
|
fun PreviewReiterScreen_Editing() {
|
||||||
|
val mockReiter = Reiter(
|
||||||
|
id = "1",
|
||||||
|
vorname = "Stefan",
|
||||||
|
nachname = "Möbius",
|
||||||
|
satznummer = "123456",
|
||||||
|
lizenz = LizenzKlasse.R2D2,
|
||||||
|
sparte = Sparte.DRESSUR,
|
||||||
|
status = ReiterStatus.AKTIV
|
||||||
|
)
|
||||||
|
val viewModel = PreviewReiterViewModel(
|
||||||
|
ReiterUiState(
|
||||||
|
searchResults = listOf(mockReiter),
|
||||||
|
selectedReiter = mockReiter,
|
||||||
|
isEditing = true,
|
||||||
|
editVorname = mockReiter.vorname,
|
||||||
|
editName = mockReiter.nachname,
|
||||||
|
editLizenz = mockReiter.lizenz,
|
||||||
|
editSparte = mockReiter.sparte,
|
||||||
|
editStatus = mockReiter.status
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
MaterialTheme {
|
||||||
|
ReiterScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentPreview
|
||||||
|
@Composable
|
||||||
|
fun PreviewReiterScreen_Empty() {
|
||||||
|
val viewModel = PreviewReiterViewModel(ReiterUiState())
|
||||||
|
MaterialTheme {
|
||||||
|
ReiterScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
+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
-4
@@ -1,6 +1,7 @@
|
|||||||
package at.mocode.veranstaltung.feature.presentation
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
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
|
||||||
@@ -9,8 +10,8 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -39,6 +40,7 @@ fun AdminUebersichtScreen(
|
|||||||
onVeranstalterAuswahl: () -> Unit,
|
onVeranstalterAuswahl: () -> Unit,
|
||||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||||
onPingService: () -> Unit = {},
|
onPingService: () -> Unit = {},
|
||||||
|
onVereineOeffnen: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
// Placeholder-Daten für die UI-Struktur (sichtbar als Cards)
|
// Placeholder-Daten für die UI-Struktur (sichtbar als Cards)
|
||||||
val sample = listOf(
|
val sample = listOf(
|
||||||
@@ -66,6 +68,7 @@ fun AdminUebersichtScreen(
|
|||||||
inVorbereitung = 0,
|
inVorbereitung = 0,
|
||||||
gesamt = 0,
|
gesamt = 0,
|
||||||
archiv = 0,
|
archiv = 0,
|
||||||
|
onVereineClick = onVereineOeffnen
|
||||||
)
|
)
|
||||||
|
|
||||||
// Toolbar
|
// Toolbar
|
||||||
@@ -155,6 +158,7 @@ private fun KpiKachelRow(
|
|||||||
inVorbereitung: Int,
|
inVorbereitung: Int,
|
||||||
gesamt: Int,
|
gesamt: Int,
|
||||||
archiv: Int,
|
archiv: Int,
|
||||||
|
onVereineClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -175,10 +179,10 @@ private fun KpiKachelRow(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
KpiKachel(
|
KpiKachel(
|
||||||
label = "GESAMT",
|
label = "VEREINE",
|
||||||
wert = gesamt.toString(),
|
wert = "4", // Mock
|
||||||
akzentFarbe = Color(0xFF6B7280),
|
akzentFarbe = Color(0xFF6B7280),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f).clickable { onVereineClick() },
|
||||||
)
|
)
|
||||||
KpiKachel(
|
KpiKachel(
|
||||||
label = "ARCHIV",
|
label = "ARCHIV",
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Feature-Modul: Vereins-Verwaltung (Desktop-only)
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "at.mocode.clients"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
sourceSets {
|
||||||
|
jvmMain.dependencies {
|
||||||
|
implementation(projects.frontend.core.designSystem)
|
||||||
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(projects.frontend.core.navigation)
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
|
implementation(libs.bundles.kmp.common)
|
||||||
|
implementation(libs.koin.core)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package at.mocode.frontend.features.verein.domain
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-Modell für einen Verein.
|
||||||
|
*/
|
||||||
|
data class Verein(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val langname: String? = null,
|
||||||
|
val oepsNr: String? = null,
|
||||||
|
val ort: String? = null,
|
||||||
|
val plz: String? = null,
|
||||||
|
val land: String = "AUT",
|
||||||
|
val status: VereinStatus = VereinStatus.AKTIV
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class VereinStatus(val label: String, val color: Color) {
|
||||||
|
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||||
|
RUHEND("Ruhend", Color(0xFFE65100)),
|
||||||
|
AUFGELOEST("Aufgelöst", Color(0xFFC62828))
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
package at.mocode.frontend.features.verein.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
import at.mocode.frontend.features.verein.domain.Verein
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VereinScreen(
|
||||||
|
viewModel: VereinViewModel
|
||||||
|
) {
|
||||||
|
val uiState = viewModel.uiState
|
||||||
|
|
||||||
|
MsMasterDetailLayout(
|
||||||
|
master = {
|
||||||
|
VereinListContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
|
onVereinSelected = viewModel::selectVerein,
|
||||||
|
onAddNew = viewModel::onAddNew
|
||||||
|
)
|
||||||
|
},
|
||||||
|
detail = {
|
||||||
|
if (uiState.isEditing) {
|
||||||
|
VereinEditorContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onNameChange = viewModel::onEditNameChange,
|
||||||
|
onLangnameChange = viewModel::onEditLangnameChange,
|
||||||
|
onOepsNrChange = viewModel::onEditOepsNrChange,
|
||||||
|
onOrtChange = viewModel::onEditOrtChange,
|
||||||
|
onPlzChange = viewModel::onEditPlzChange,
|
||||||
|
onStatusChange = viewModel::onEditStatusChange,
|
||||||
|
onSave = viewModel::onSave,
|
||||||
|
onCancel = viewModel::onCancel
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PlaceholderContent(
|
||||||
|
title = "Kein Verein ausgewählt",
|
||||||
|
subtitle = "Wählen Sie einen Verein aus der Liste aus oder legen Sie einen neuen an."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VereinListContent(
|
||||||
|
uiState: VereinUiState,
|
||||||
|
onSearchChange: (String) -> Unit,
|
||||||
|
onVereinSelected: (Verein) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsFilterBar(
|
||||||
|
searchQuery = uiState.searchQuery,
|
||||||
|
onSearchQueryChange = onSearchChange,
|
||||||
|
resultCount = uiState.searchResults.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
text = "Neu",
|
||||||
|
onClick = onAddNew,
|
||||||
|
variant = ButtonVariant.PRIMARY,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
MsDataTable(
|
||||||
|
items = uiState.searchResults,
|
||||||
|
columns = listOf(
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Name",
|
||||||
|
weight = 1.5f,
|
||||||
|
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Ort",
|
||||||
|
weight = 1f,
|
||||||
|
cellRenderer = { Text(it.ort ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "OePS-Nr",
|
||||||
|
width = 100.dp,
|
||||||
|
cellRenderer = { Text(it.oepsNr ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Status",
|
||||||
|
width = 100.dp,
|
||||||
|
cellRenderer = {
|
||||||
|
MsStatusBadge(
|
||||||
|
text = it.status.label,
|
||||||
|
containerColor = it.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = it.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onRowClick = onVereinSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VereinEditorContent(
|
||||||
|
uiState: VereinUiState,
|
||||||
|
onNameChange: (String) -> Unit,
|
||||||
|
onLangnameChange: (String) -> Unit,
|
||||||
|
onOepsNrChange: (String) -> Unit,
|
||||||
|
onOrtChange: (String) -> Unit,
|
||||||
|
onPlzChange: (String) -> Unit,
|
||||||
|
onStatusChange: (VereinStatus) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsActionToolbar(
|
||||||
|
title = if (uiState.selectedVerein == null) "Neuer Verein" else "Verein Details",
|
||||||
|
onSave = onSave,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editName,
|
||||||
|
onValueChange = onNameChange,
|
||||||
|
label = "Name (Kurz)",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editLangname,
|
||||||
|
onValueChange = onLangnameChange,
|
||||||
|
label = "Vollständiger Name",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editOepsNr,
|
||||||
|
onValueChange = onOepsNrChange,
|
||||||
|
label = "OePS-Nr",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MsEnumDropdown(
|
||||||
|
label = "Status",
|
||||||
|
options = VereinStatus.entries.toTypedArray(),
|
||||||
|
selectedOption = uiState.editStatus,
|
||||||
|
onOptionSelected = onStatusChange,
|
||||||
|
optionLabel = { it.label },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editPlz,
|
||||||
|
onValueChange = onPlzChange,
|
||||||
|
label = "PLZ",
|
||||||
|
modifier = Modifier.weight(0.3f)
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editOrt,
|
||||||
|
onValueChange = onOrtChange,
|
||||||
|
label = "Ort",
|
||||||
|
modifier = Modifier.weight(0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
package at.mocode.frontend.features.verein.presentation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import at.mocode.frontend.features.verein.domain.Verein
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-State für die Vereins-Verwaltung.
|
||||||
|
*/
|
||||||
|
data class VereinUiState(
|
||||||
|
val allVereine: List<Verein> = emptyList(),
|
||||||
|
val searchResults: List<Verein> = emptyList(),
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val selectedVerein: Verein? = null,
|
||||||
|
val isEditing: Boolean = false,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val editName: String = "",
|
||||||
|
val editLangname: String = "",
|
||||||
|
val editOepsNr: String = "",
|
||||||
|
val editOrt: String = "",
|
||||||
|
val editPlz: String = "",
|
||||||
|
val editStatus: VereinStatus = VereinStatus.AKTIV
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel für die Vereins-Verwaltung.
|
||||||
|
*/
|
||||||
|
open class VereinViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||||
|
var uiState by mutableStateOf(VereinUiState())
|
||||||
|
protected set
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (initialLoad) {
|
||||||
|
loadVereine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadVereine() {
|
||||||
|
val mockData = listOf(
|
||||||
|
Verein("1", "URV Neumarkt", "Union Reit- und Fahrverein Neumarkt", "4-201", "Neumarkt", "4212"),
|
||||||
|
Verein("2", "RV Linz", "Reitverein Linz-Ebelsberg", "4-001", "Linz", "4030"),
|
||||||
|
Verein("3", "RC Stadl-Paura", "Reitclub Pferdewelt Stadl-Paura", "4-100", "Stadl-Paura", "4650"),
|
||||||
|
Verein("4", "Union Reitverein X", null, "1-123", "Wien", "1010", status = VereinStatus.RUHEND)
|
||||||
|
)
|
||||||
|
uiState = uiState.copy(
|
||||||
|
allVereine = mockData,
|
||||||
|
searchResults = mockData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchQueryChange(query: String) {
|
||||||
|
uiState = uiState.copy(searchQuery = query)
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterResults() {
|
||||||
|
val query = uiState.searchQuery.lowercase()
|
||||||
|
val filtered = if (query.isEmpty()) {
|
||||||
|
uiState.allVereine
|
||||||
|
} else {
|
||||||
|
uiState.allVereine.filter {
|
||||||
|
it.name.lowercase().contains(query) ||
|
||||||
|
it.oepsNr?.lowercase()?.contains(query) == true ||
|
||||||
|
it.ort?.lowercase()?.contains(query) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiState = uiState.copy(searchResults = filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectVerein(verein: Verein) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedVerein = verein,
|
||||||
|
isEditing = true,
|
||||||
|
editName = verein.name,
|
||||||
|
editLangname = verein.langname ?: "",
|
||||||
|
editOepsNr = verein.oepsNr ?: "",
|
||||||
|
editOrt = verein.ort ?: "",
|
||||||
|
editPlz = verein.plz ?: "",
|
||||||
|
editStatus = verein.status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditNameChange(value: String) {
|
||||||
|
uiState = uiState.copy(editName = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditLangnameChange(value: String) {
|
||||||
|
uiState = uiState.copy(editLangname = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditOepsNrChange(value: String) {
|
||||||
|
uiState = uiState.copy(editOepsNr = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditOrtChange(value: String) {
|
||||||
|
uiState = uiState.copy(editOrt = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditPlzChange(value: String) {
|
||||||
|
uiState = uiState.copy(editPlz = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditStatusChange(value: VereinStatus) {
|
||||||
|
uiState = uiState.copy(editStatus = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSave() {
|
||||||
|
// Mock-Speichern
|
||||||
|
uiState = uiState.copy(isEditing = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCancel() {
|
||||||
|
uiState = uiState.copy(isEditing = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddNew() {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedVerein = null,
|
||||||
|
isEditing = true,
|
||||||
|
editName = "",
|
||||||
|
editLangname = "",
|
||||||
|
editOepsNr = "",
|
||||||
|
editOrt = "",
|
||||||
|
editPlz = "",
|
||||||
|
editStatus = VereinStatus.AKTIV
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package at.mocode.frontend.features.verein.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||||
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val vereinFeatureModule = module {
|
||||||
|
viewModelOf(::VereinViewModel)
|
||||||
|
}
|
||||||
@@ -35,7 +35,10 @@ kotlin {
|
|||||||
implementation(projects.frontend.features.veranstaltungFeature)
|
implementation(projects.frontend.features.veranstaltungFeature)
|
||||||
implementation(projects.frontend.features.turnierFeature)
|
implementation(projects.frontend.features.turnierFeature)
|
||||||
implementation(project(":frontend:features:profile-feature"))
|
implementation(project(":frontend:features:profile-feature"))
|
||||||
|
implementation(project(":frontend:features:reiter-feature"))
|
||||||
|
implementation(project(":frontend:features:pferde-feature"))
|
||||||
implementation(project(":frontend:features:billing-feature"))
|
implementation(project(":frontend:features:billing-feature"))
|
||||||
|
implementation(project(":frontend:features:verein-feature"))
|
||||||
|
|
||||||
// Compose Desktop
|
// Compose Desktop
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
|
|||||||
+9
-3
@@ -37,10 +37,16 @@ fun DesktopApp() {
|
|||||||
val authState by authTokenManager.authState.collectAsState()
|
val authState by authTokenManager.authState.collectAsState()
|
||||||
|
|
||||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
|
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
|
||||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Onboarding) {
|
// Vision_03 Update: Wir starten direkt in der Veranstaltungs-Übersicht (Offline-First)
|
||||||
|
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Veranstaltungen
|
||||||
|
&& 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
|
||||||
|
) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Wenn noch keine Authentifizierung vorhanden ist, zuerst Onboarding anzeigen
|
// Standard: Direkt zur Veranstaltungs-Übersicht (Offline-First-Modus)
|
||||||
nav.navigateToScreen(AppScreen.Onboarding)
|
nav.navigateToScreen(AppScreen.Veranstaltungen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-11
@@ -5,12 +5,6 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.window.singleWindowApplication
|
import androidx.compose.ui.window.singleWindowApplication
|
||||||
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
|
||||||
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
|
|
||||||
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
|
|
||||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
|
||||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hot-Reload Preview Entry Point
|
* Hot-Reload Preview Entry Point
|
||||||
@@ -31,6 +25,13 @@ fun main() = singleWindowApplication(title = "🔥 Hot-Reload Preview") {
|
|||||||
private fun PreviewContent() {
|
private fun PreviewContent() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
Surface {
|
Surface {
|
||||||
|
|
||||||
|
// --- REITER ---
|
||||||
|
// ReiterScreen(viewModel = ReiterViewModel())
|
||||||
|
|
||||||
|
// --- PFERDE ---
|
||||||
|
// PferdeScreen(viewModel = PferdeViewModel())
|
||||||
|
|
||||||
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
||||||
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
||||||
// VeranstalterNeuScreen(onBack = {}, onSave = {})
|
// VeranstalterNeuScreen(onBack = {}, onSave = {})
|
||||||
@@ -40,11 +41,11 @@ private fun PreviewContent() {
|
|||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Standard: AdminUebersichtScreen (Startseite nach Login)
|
// Standard: AdminUebersichtScreen (Startseite nach Login)
|
||||||
AdminUebersichtScreen(
|
// AdminUebersichtScreen(
|
||||||
onVeranstalterAuswahl = {},
|
// onVeranstalterAuswahl = {},
|
||||||
onVeranstaltungOeffnen = {},
|
// onVeranstaltungOeffnen = {},
|
||||||
onPingService = {}
|
// onPingService = {}
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import at.mocode.frontend.core.network.networkModule
|
|||||||
import at.mocode.frontend.core.sync.di.syncModule
|
import at.mocode.frontend.core.sync.di.syncModule
|
||||||
import at.mocode.frontend.features.billing.di.billingModule
|
import at.mocode.frontend.features.billing.di.billingModule
|
||||||
import at.mocode.frontend.features.profile.di.profileModule
|
import at.mocode.frontend.features.profile.di.profileModule
|
||||||
|
import at.mocode.frontend.features.verein.di.vereinFeatureModule
|
||||||
import at.mocode.nennung.feature.di.nennungFeatureModule
|
import at.mocode.nennung.feature.di.nennungFeatureModule
|
||||||
import at.mocode.ping.feature.di.pingFeatureModule
|
import at.mocode.ping.feature.di.pingFeatureModule
|
||||||
import at.mocode.zns.feature.di.znsImportModule
|
import at.mocode.zns.feature.di.znsImportModule
|
||||||
@@ -35,10 +36,13 @@ fun main() = application {
|
|||||||
znsImportModule,
|
znsImportModule,
|
||||||
profileModule,
|
profileModule,
|
||||||
billingModule,
|
billingModule,
|
||||||
|
vereinFeatureModule,
|
||||||
desktopModule,
|
desktopModule,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
println("[DesktopApp] KOIN initialisiert")
|
println("[DesktopApp] KOIN initialisiert")
|
||||||
|
// Testdaten für Prototyp laden
|
||||||
|
at.mocode.desktop.v2.StoreV2.seed()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("[DesktopApp] Koin-Warnung: ${e.message}")
|
println("[DesktopApp] Koin-Warnung: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-39
@@ -5,10 +5,8 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
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.Logout
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -20,18 +18,18 @@ import androidx.compose.ui.unit.sp
|
|||||||
import at.mocode.frontend.core.navigation.AppScreen
|
import at.mocode.frontend.core.navigation.AppScreen
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
||||||
|
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
||||||
|
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.VeranstalterAuswahlV2
|
|
||||||
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
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||||
private val TopBarColor = Color(0xFF1E3A8A)
|
private val TopBarColor = Color(0xFF1E3A8A)
|
||||||
@@ -108,7 +106,7 @@ private fun DesktopTopBar(
|
|||||||
|
|
||||||
// Root-Link
|
// Root-Link
|
||||||
Text(
|
Text(
|
||||||
text = "🏠 Admin - Verwaltung",
|
text = "Veranstaltungen",
|
||||||
color = TopBarTextColor,
|
color = TopBarTextColor,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -243,18 +241,21 @@ private fun DesktopTopBar(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AppScreen.Vereine -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Vereine",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout rechts
|
// Logout wurde auf Kundenwunsch entfernt
|
||||||
IconButton(onClick = onLogout) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
|
||||||
contentDescription = "Abmelden",
|
|
||||||
tint = TopBarTextColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,19 +302,17 @@ private fun DesktopContentArea(
|
|||||||
|
|
||||||
// Root-Screen: Leitet in V2-Fluss ab
|
// Root-Screen: Leitet in V2-Fluss ab
|
||||||
is AppScreen.Veranstaltungen -> {
|
is AppScreen.Veranstaltungen -> {
|
||||||
// Direkt zur Veranstalter-Auswahl V2
|
at.mocode.desktop.v2.VeranstaltungenUebersichtV2(
|
||||||
VeranstalterAuswahlV2(
|
onEventOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, eId)) },
|
||||||
onZurueck = { /* bleibt root */ },
|
onNewEvent = { onNavigate(AppScreen.VeranstaltungKonfig()) }
|
||||||
onWeiter = { vId -> onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
|
||||||
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlV2(
|
is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2(
|
||||||
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||||
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
|
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
||||||
@@ -338,19 +337,15 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungKonfig -> {
|
is AppScreen.VeranstaltungKonfig -> {
|
||||||
val vId = currentScreen.veranstalterId
|
val vId = currentScreen.veranstalterId
|
||||||
// V2: Validierung über StoreV2
|
// Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard
|
||||||
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) {
|
at.mocode.desktop.v2.VeranstaltungKonfigV2(
|
||||||
InvalidContextNotice(
|
veranstalterId = vId,
|
||||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
onBack = {
|
||||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
if (vId == 0L) onNavigate(AppScreen.Veranstaltungen)
|
||||||
)
|
else onNavigate(AppScreen.VeranstalterDetail(vId))
|
||||||
} else {
|
},
|
||||||
at.mocode.desktop.v2.VeranstaltungKonfigV2(
|
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) }
|
||||||
veranstalterId = vId,
|
)
|
||||||
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
|
||||||
onSaved = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungUebersicht -> {
|
is AppScreen.VeranstaltungUebersicht -> {
|
||||||
val vId = currentScreen.veranstalterId
|
val vId = currentScreen.veranstalterId
|
||||||
@@ -391,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) }
|
||||||
@@ -400,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)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,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)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,6 +440,14 @@ private fun DesktopContentArea(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vereins-Verwaltung
|
||||||
|
is AppScreen.Vereine -> {
|
||||||
|
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||||
|
VereinScreen(
|
||||||
|
viewModel = vereinViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback → Root
|
// Fallback → Root
|
||||||
else -> AdminUebersichtScreen(
|
else -> AdminUebersichtScreen(
|
||||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
|
|||||||
+88
-1
@@ -12,21 +12,106 @@ data class Verein(
|
|||||||
|
|
||||||
data class VeranstaltungV2(
|
data class VeranstaltungV2(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val veranstalterId: Long,
|
var veranstalterId: Long,
|
||||||
var titel: String,
|
var titel: String,
|
||||||
var datumVon: String,
|
var datumVon: String,
|
||||||
var datumBis: String?,
|
var datumBis: String?,
|
||||||
var status: String = "In Vorbereitung",
|
var status: String = "In Vorbereitung",
|
||||||
|
var beschreibung: String = "",
|
||||||
|
var untertitel: String = "",
|
||||||
|
var ort: String = "",
|
||||||
|
var logoUrl: String? = null,
|
||||||
|
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
object StoreV2 {
|
object StoreV2 {
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
|
||||||
val vereine: SnapshotStateList<Verein> = mutableStateListOf(
|
val vereine: SnapshotStateList<Verein> = mutableStateListOf(
|
||||||
Verein(1, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."),
|
Verein(1, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."),
|
||||||
Verein(2, "Pferdesportverein Linz", "V-OOE-0002", "Linz"),
|
Verein(2, "Pferdesportverein Linz", "V-OOE-0002", "Linz"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun addVerein(name: String, oeps: String, ort: String): Long {
|
||||||
|
val id = (vereine.maxOfOrNull { it.id } ?: 0) + 1
|
||||||
|
vereine.add(Verein(id, name, oeps, ort))
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
private val veranstaltungen: MutableMap<Long, SnapshotStateList<VeranstaltungV2>> = mutableMapOf()
|
private val veranstaltungen: MutableMap<Long, SnapshotStateList<VeranstaltungV2>> = mutableMapOf()
|
||||||
|
|
||||||
|
fun seed() {
|
||||||
|
// Falls bereits Daten da sind (außer den statischen Vereinen), nichts tun
|
||||||
|
if (veranstaltungen.isNotEmpty()) return
|
||||||
|
|
||||||
|
// 1. Neumarkt 2026 (ID 100)
|
||||||
|
val neumarktId = 100L
|
||||||
|
addEventFirst(
|
||||||
|
1, VeranstaltungV2(
|
||||||
|
id = neumarktId,
|
||||||
|
veranstalterId = 1,
|
||||||
|
titel = "Frühjahrsturnier Neumarkt/M. 2026",
|
||||||
|
datumVon = "2026-04-10",
|
||||||
|
datumBis = "2026-04-12",
|
||||||
|
status = "Nennungsphase",
|
||||||
|
beschreibung = "Traditionelles Frühjahrsturnier mit Spring- und Dressurprüfungen bis Klasse LM."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
TurnierStoreV2.add(
|
||||||
|
neumarktId,
|
||||||
|
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(
|
||||||
|
neumarktId,
|
||||||
|
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)
|
||||||
|
val linzId = 200L
|
||||||
|
addEventFirst(
|
||||||
|
2, VeranstaltungV2(
|
||||||
|
id = linzId,
|
||||||
|
veranstalterId = 2,
|
||||||
|
titel = "Linzer Pferdefestival",
|
||||||
|
datumVon = "2026-05-20",
|
||||||
|
datumBis = "2026-05-24",
|
||||||
|
status = "In Vorbereitung",
|
||||||
|
beschreibung = "Großes Reit-Event am Ebelsberger Schlosspark."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
addEventFirst(
|
||||||
|
1, VeranstaltungV2(
|
||||||
|
id = 300L,
|
||||||
|
veranstalterId = 1,
|
||||||
|
titel = "Herbst-Turnier 2025",
|
||||||
|
datumVon = "2025-09-15",
|
||||||
|
datumBis = "2025-09-17",
|
||||||
|
status = "Abgeschlossen"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun eventsFor(vereinId: Long): SnapshotStateList<VeranstaltungV2> =
|
fun eventsFor(vereinId: Long): SnapshotStateList<VeranstaltungV2> =
|
||||||
veranstaltungen.getOrPut(vereinId) { mutableStateListOf() }
|
veranstaltungen.getOrPut(vereinId) { mutableStateListOf() }
|
||||||
|
|
||||||
@@ -39,4 +124,6 @@ object StoreV2 {
|
|||||||
val idx = list.indexOfFirst { it.id == veranstaltungId }
|
val idx = list.indexOfFirst { it.id == veranstaltungId }
|
||||||
if (idx >= 0) list.removeAt(idx)
|
if (idx >= 0) list.removeAt(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun allEvents(): List<VeranstaltungV2> = veranstaltungen.values.flatten()
|
||||||
}
|
}
|
||||||
|
|||||||
+1390
-116
File diff suppressed because it is too large
Load Diff
@@ -99,7 +99,7 @@ foojayResolver = "1.0.0"
|
|||||||
benManesVersions = "0.51.0"
|
benManesVersions = "0.51.0"
|
||||||
detekt = "1.23.6"
|
detekt = "1.23.6"
|
||||||
ktlint = "12.1.1"
|
ktlint = "12.1.1"
|
||||||
dokka = "2.1.0"
|
dokka = "2.2.0"
|
||||||
firebaseDatabaseKtx = "22.0.1"
|
firebaseDatabaseKtx = "22.0.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ include(":frontend:features:zns-import-feature")
|
|||||||
include(":frontend:features:veranstalter-feature")
|
include(":frontend:features:veranstalter-feature")
|
||||||
include(":frontend:features:veranstaltung-feature")
|
include(":frontend:features:veranstaltung-feature")
|
||||||
include(":frontend:features:profile-feature")
|
include(":frontend:features:profile-feature")
|
||||||
|
include(":frontend:features:reiter-feature")
|
||||||
|
include(":frontend:features:pferde-feature")
|
||||||
|
include(":frontend:features:verein-feature")
|
||||||
include(":frontend:features:turnier-feature")
|
include(":frontend:features:turnier-feature")
|
||||||
include(":frontend:features:billing-feature")
|
include(":frontend:features:billing-feature")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user