Compare commits
2 Commits
a72953cea7
...
c2b3b5889f
| Author | SHA1 | Date | |
|---|---|---|---|
| c2b3b5889f | |||
| 1d393fdefe |
|
|
@ -18,7 +18,7 @@ env:
|
|||
REGISTRY_INTERNAL: 10.0.0.22:3000
|
||||
IMAGE_PREFIX: mocode-software/meldestelle
|
||||
JAVA_VERSION: "25"
|
||||
GRADLE_VERSION: "9.3.1"
|
||||
GRADLE_VERSION: "9.4.0"
|
||||
KEYCLOAK_IMAGE_TAG: "26.5.5"
|
||||
# Workers auf 4 limitiert: verhindert OOM auf dem 16GB Runner (VM 102)
|
||||
GRADLE_OPTS: "-Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4"
|
||||
|
|
|
|||
163
docs/06_Frontend/ARCHITECTURE_RULES.md
Normal file
163
docs/06_Frontend/ARCHITECTURE_RULES.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Frontend-Architektur-Richtlinien
|
||||
|
||||
> **Status:** Verbindlich ab 26.03.2026
|
||||
> **Zuständig:** 🏗️ Lead Architect
|
||||
> **Zweck:** Verhindert Architektur-Drift und inkonsistente Schichtentrennung.
|
||||
|
||||
---
|
||||
|
||||
## Die 3 Schichten
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── core/ ← Infrastruktur (plattformübergreifend, kein Business-Code)
|
||||
├── features/ ← Fachliche Bausteine (je ein Bounded Context)
|
||||
└── shells/ ← Ausführbare Apps (nur Verdrahtung, kein Fach-UI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schicht 1: `core/`
|
||||
|
||||
### Aufgabe
|
||||
|
||||
Gemeinsame Infrastruktur, die von **allen** Features und Shells genutzt wird.
|
||||
|
||||
### Module
|
||||
|
||||
| Modul | Inhalt |
|
||||
|-----------------|-----------------------------------------------------------------|
|
||||
| `auth` | Login, Token-Management, OIDC/PKCE, `LoginScreen` |
|
||||
| `design-system` | Farben, Typografie, gemeinsame UI-Komponenten, `SharedUiModels` |
|
||||
| `domain` | Gemeinsame Domain-Modelle (plattformübergreifend) |
|
||||
| `navigation` | `AppScreen`-Sealed-Class (einzige Wahrheit über alle Routen) |
|
||||
| `network` | Ktor-Client, `NetworkConfig` |
|
||||
| `local-db` | SQLDelight/Room-Setup, `DatabaseProvider` |
|
||||
| `sync` | Offline-Sync-Infrastruktur |
|
||||
|
||||
### Regeln
|
||||
|
||||
- ✅ Darf importieren: externe Libraries, andere `core`-Module (keine Zyklen)
|
||||
- ❌ Darf NICHT importieren: `features/*`, `shells/*`
|
||||
- ❌ Kein Business-Code, keine fachlichen Screens
|
||||
|
||||
---
|
||||
|
||||
## Schicht 2: `features/`
|
||||
|
||||
### Aufgabe
|
||||
|
||||
Jedes Feature kapselt **einen Bounded Context** vollständig: Daten, Logik und UI.
|
||||
|
||||
### Pflicht-Struktur eines Feature-Moduls
|
||||
|
||||
```
|
||||
features/<name>-feature/
|
||||
└── src/
|
||||
└── jvmMain/kotlin/at/mocode/<name>/feature/
|
||||
├── data/ ← Repository, API-Client
|
||||
├── domain/ ← Modelle, Use Cases
|
||||
├── presentation/ ← ViewModel + Screen-Composables ← PFLICHT
|
||||
└── di/ ← Koin-Module
|
||||
```
|
||||
|
||||
### Vorhandene Features
|
||||
|
||||
| Feature | Bounded Context |
|
||||
|-------------------------|-----------------------------------------------|
|
||||
| `ping-feature` | Verbindungstest / Sync-Status |
|
||||
| `nennung-feature` | Nennungs-Erfassung am Turnier |
|
||||
| `zns-import-feature` | ZNS-Stammdaten-Import |
|
||||
| `veranstalter-feature` | Veranstalter-Auswahl, -Detail, -Neuanlage |
|
||||
| `veranstaltung-feature` | Veranstaltungs-Übersicht, -Detail, -Neuanlage |
|
||||
| `turnier-feature` | Turnier-Detail, alle Tabs, Akteure |
|
||||
|
||||
### Regeln
|
||||
|
||||
- ✅ Darf importieren: `core/*`
|
||||
- ❌ Darf NICHT importieren: andere `features/*`, `shells/*`
|
||||
- ✅ **Jedes Feature MUSS seinen eigenen Screen in `presentation/` haben**
|
||||
- ❌ Screen-Composables gehören NICHT in den Shell
|
||||
|
||||
---
|
||||
|
||||
## Schicht 3: `shells/`
|
||||
|
||||
### Aufgabe
|
||||
|
||||
Einstiegspunkt einer konkreten App. Verdrahtet Features und Core zu einer lauffähigen Anwendung.
|
||||
|
||||
### Erlaubter Inhalt im Shell
|
||||
|
||||
```
|
||||
shells/<name>/
|
||||
└── src/jvmMain/kotlin/at/mocode/desktop/
|
||||
├── main.kt ← App-Einstiegspunkt, Koin-Init
|
||||
├── DesktopApp.kt ← Root-Composable, Login-Gate
|
||||
├── di/DesktopModule.kt ← Shell-spezifische DI
|
||||
├── navigation/ ← Navigation-Port (optional)
|
||||
└── screens/
|
||||
├── layout/DesktopMainLayout.kt ← Navigation + Layout-Gerüst
|
||||
└── preview/ScreenPreviews.kt ← @Preview-Funktionen (IDE-only)
|
||||
```
|
||||
|
||||
### Regeln
|
||||
|
||||
- ✅ Darf importieren: `core/*`, `features/*`
|
||||
- ✅ Darf enthalten: `main.kt`, `DesktopApp.kt`, DI-Verdrahtung, Layout, Previews
|
||||
- ❌ Darf NICHT enthalten: fachliche Screen-Composables (gehören in Features)
|
||||
- ❌ Darf NICHT enthalten: ViewModels, Repositories, Business-Logik
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeits-Diagramm
|
||||
|
||||
```
|
||||
shells/meldestelle-desktop
|
||||
├── core/auth
|
||||
├── core/design-system
|
||||
├── core/domain
|
||||
├── core/navigation
|
||||
├── core/network
|
||||
├── core/local-db
|
||||
├── core/sync
|
||||
├── features/ping-feature
|
||||
├── features/nennung-feature
|
||||
├── features/zns-import-feature
|
||||
├── features/veranstalter-feature
|
||||
├── features/veranstaltung-feature
|
||||
└── features/turnier-feature
|
||||
|
||||
features/* → core/* (nur)
|
||||
core/* → (keine internen Abhängigkeiten außer erlaubte core-zu-core)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checkliste: Neues Feature anlegen
|
||||
|
||||
1. `frontend/features/<name>-feature/` Verzeichnis anlegen
|
||||
2. `build.gradle.kts` nach Vorlage `nennung-feature` erstellen
|
||||
3. Eintrag in `settings.gradle.kts` unter `// --- FEATURES ---` hinzufügen
|
||||
4. Eintrag in `shells/meldestelle-desktop/build.gradle.kts` unter `// Feature-Module` hinzufügen
|
||||
5. Screen in `presentation/` implementieren
|
||||
6. DI-Modul in `di/` implementieren
|
||||
7. DI-Modul in `shells/.../main.kt` registrieren
|
||||
8. Route in `core/navigation/AppScreen.kt` eintragen
|
||||
9. Navigation-Case in `shells/.../screens/layout/DesktopMainLayout.kt` eintragen
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (verboten)
|
||||
|
||||
| Anti-Pattern | Warum verboten |
|
||||
|------------------------------------|------------------------------------------------------|
|
||||
| Screen-Composable direkt im Shell | Verletzt Schichttrennung, nicht wiederverwendbar |
|
||||
| Feature importiert anderes Feature | Erzeugt Kopplung, verhindert unabhängige Entwicklung |
|
||||
| `core` importiert `features` | Zirkuläre Abhängigkeit |
|
||||
| Shared-Modelle im Shell definieren | Gehören in `core/design-system` oder `core/domain` |
|
||||
| ViewModel im Shell | Gehört ins Feature |
|
||||
|
||||
---
|
||||
|
||||
*Letzte Aktualisierung: 26.03.2026 — nach Architektur-Refactor (Screens aus Shell in Features verschoben)*
|
||||
200
docs/06_Frontend/Navigation_Routing_Diagramm.md
Normal file
200
docs/06_Frontend/Navigation_Routing_Diagramm.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-26
|
||||
---
|
||||
|
||||
# Navigation & Routing Diagramm — Meldestelle Desktop
|
||||
|
||||
🏗️ **[Lead Architect]** | 26. März 2026
|
||||
|
||||
Dieses Dokument visualisiert alle Screens und Navigationsübergänge der Compose Desktop App.
|
||||
Generiert aus: `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/`
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht: NavRail-Einstiegspunkte
|
||||
|
||||
Die linke Navigationsleiste (NavRail) bietet folgende Direkteinstiege:
|
||||
|
||||
| Icon | Label | Ziel-Screen | Status |
|
||||
|------|-------------------|--------------------|-----------------------------|
|
||||
| 📅 | Veranstaltungen | `Veranstaltungen` | ✅ Implementiert |
|
||||
| 🏇 | Reiter | `Reiter` | ⬜ Placeholder |
|
||||
| 🐴 | Pferde | `Pferde` | ⬜ Placeholder |
|
||||
| 👤 | Funktionäre | `Funktionaere` | ⬜ Placeholder |
|
||||
| 🏆 | Meisterschaften | `Meisterschaften` | ⬜ Placeholder |
|
||||
| 🥇 | Cups | `Cups` | ⬜ Placeholder |
|
||||
| 📥 | Stammdaten-Import | `StammdatenImport` | 🟡 UI fertig, Polling offen |
|
||||
|
||||
---
|
||||
|
||||
## 2. Vollständiges Navigationsfluss-Diagramm
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% ─── App-Start & Auth ───────────────────────────────────────────
|
||||
START([App Start]) --> LOGIN
|
||||
|
||||
LOGIN["🔐 Login\n/auth/login"]
|
||||
LOGIN -->|"onSuccess (returnTo)"| VERANSTALTUNGEN
|
||||
|
||||
AUTH_GUARD{{"🛡️ Auth Guard\n(nicht eingeloggt?)"}}
|
||||
AUTH_GUARD -->|"nicht authentifiziert"| LOGIN
|
||||
|
||||
%% ─── NavRail Top-Level ──────────────────────────────────────────
|
||||
NAVRAIL(["🗂️ NavRail"])
|
||||
NAVRAIL --> VERANSTALTUNGEN
|
||||
NAVRAIL --> REITER
|
||||
NAVRAIL --> PFERDE
|
||||
NAVRAIL --> FUNKTIONAERE
|
||||
NAVRAIL --> MEISTERSCHAFTEN
|
||||
NAVRAIL --> CUPS
|
||||
NAVRAIL --> STAMMDATEN_IMPORT
|
||||
|
||||
%% ─── Veranstaltungen-Flow ───────────────────────────────────────
|
||||
VERANSTALTUNGEN["📅 Veranstaltungen\n(AdminUebersichtScreen)\n/veranstaltungen"]
|
||||
VERANSTALTUNGEN -->|"+ Neue Veranstaltung"| VERANSTALTER_AUSWAHL
|
||||
VERANSTALTUNGEN -->|"Veranstaltung öffnen (id)"| VERANSTALTUNG_DETAIL
|
||||
|
||||
VERANSTALTER_AUSWAHL["🏢 Veranstalter auswählen\n/veranstalter/auswahl"]
|
||||
VERANSTALTER_AUSWAHL -->|"Zurück"| VERANSTALTUNGEN
|
||||
VERANSTALTER_AUSWAHL -->|"Weiter (veranstalterId)"| VERANSTALTER_DETAIL
|
||||
|
||||
VERANSTALTER_DETAIL["🏢 Veranstalter Detail\n/veranstalter/{id}"]
|
||||
VERANSTALTER_DETAIL -->|"Zurück"| VERANSTALTER_AUSWAHL
|
||||
VERANSTALTER_DETAIL -->|"Veranstaltung öffnen (vId)"| VERANSTALTUNG_UEBERSICHT
|
||||
VERANSTALTER_DETAIL -->|"Neue Veranstaltung gespeichert"| VERANSTALTER_DETAIL
|
||||
|
||||
VERANSTALTUNG_UEBERSICHT["📋 Veranstaltung Übersicht\n/veranstalter/{verId}/veranstaltung/{vId}"]
|
||||
VERANSTALTUNG_UEBERSICHT -->|"Zurück"| VERANSTALTER_DETAIL
|
||||
VERANSTALTUNG_UEBERSICHT -->|"Turnier öffnen (tId)"| TURNIER_DETAIL
|
||||
VERANSTALTUNG_UEBERSICHT -->|"+ Neues Turnier"| TURNIER_NEU
|
||||
|
||||
VERANSTALTUNG_DETAIL["📄 Veranstaltung Detail\n/veranstaltung/{id}"]
|
||||
VERANSTALTUNG_DETAIL -->|"Zurück"| VERANSTALTUNGEN
|
||||
VERANSTALTUNG_DETAIL -->|"+ Neues Turnier"| TURNIER_NEU
|
||||
VERANSTALTUNG_DETAIL -->|"Turnier öffnen (tId)"| TURNIER_DETAIL
|
||||
|
||||
VERANSTALTUNG_NEU["➕ Neue Veranstaltung\n/veranstaltung/neu"]
|
||||
VERANSTALTUNG_NEU -->|"Zurück"| VERANSTALTUNGEN
|
||||
VERANSTALTUNG_NEU -->|"Speichern"| VERANSTALTUNGEN
|
||||
|
||||
TURNIER_DETAIL["🏟️ Turnier Detail\n/veranstaltung/{vId}/turnier/{tId}\n(inkl. Nennungs-Tab ⭐)"]
|
||||
TURNIER_DETAIL -->|"Zurück"| VERANSTALTUNG_DETAIL
|
||||
|
||||
TURNIER_NEU["➕ Neues Turnier\n/veranstaltung/{vId}/turnier/neu"]
|
||||
TURNIER_NEU -->|"Zurück"| VERANSTALTUNG_DETAIL
|
||||
TURNIER_NEU -->|"Speichern"| VERANSTALTUNG_DETAIL
|
||||
|
||||
%% ─── Stammdaten-Import ──────────────────────────────────────────
|
||||
STAMMDATEN_IMPORT["📥 Stammdaten Import\n/stammdaten/import\n(ZNS ZIP-Import)"]
|
||||
|
||||
%% ─── Placeholder Screens ────────────────────────────────────────
|
||||
REITER["🏇 Reiter\n/reiter\n⬜ Placeholder"]
|
||||
PFERDE["🐴 Pferde\n/pferde\n⬜ Placeholder"]
|
||||
FUNKTIONAERE["👤 Funktionäre\n/funktionaere\n⬜ Placeholder"]
|
||||
MEISTERSCHAFTEN["🏆 Meisterschaften\n/meisterschaften\n⬜ Placeholder"]
|
||||
CUPS["🥇 Cups\n/cups\n⬜ Placeholder"]
|
||||
|
||||
%% ─── Logout ─────────────────────────────────────────────────────
|
||||
LOGOUT(["🚪 Logout"])
|
||||
LOGOUT -->|"Token löschen"| LOGIN
|
||||
|
||||
%% ─── Styling ────────────────────────────────────────────────────
|
||||
style LOGIN fill:#f0a500,color:#000
|
||||
style AUTH_GUARD fill:#e74c3c,color:#fff
|
||||
style VERANSTALTUNGEN fill:#2ecc71,color:#000
|
||||
style TURNIER_DETAIL fill:#3498db,color:#fff
|
||||
style STAMMDATEN_IMPORT fill:#9b59b6,color:#fff
|
||||
style REITER fill:#bdc3c7,color:#555
|
||||
style PFERDE fill:#bdc3c7,color:#555
|
||||
style FUNKTIONAERE fill:#bdc3c7,color:#555
|
||||
style MEISTERSCHAFTEN fill:#bdc3c7,color:#555
|
||||
style CUPS fill:#bdc3c7,color:#555
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Screens nach Status
|
||||
|
||||
### ✅ Vollständig implementiert
|
||||
|
||||
| Screen | Route | Komponente |
|
||||
|-----------------------------|---------------------------------------------|-----------------------------------------|
|
||||
| Login | `/auth/login` | `LoginScreen` |
|
||||
| Veranstaltungen (Übersicht) | `/veranstaltungen` | `AdminUebersichtScreen` |
|
||||
| Veranstalter Auswahl | `/veranstalter/auswahl` | `VeranstalterAuswahlScreen` |
|
||||
| Veranstalter Detail | `/veranstalter/{id}` | `VeranstalterDetailScreen` |
|
||||
| Veranstaltung Übersicht | `/veranstalter/{verId}/veranstaltung/{vId}` | `VeranstaltungUebersichtScreen` |
|
||||
| Veranstaltung Detail | `/veranstaltung/{id}` | `VeranstaltungDetailScreen` |
|
||||
| Veranstaltung Neu | `/veranstaltung/neu` | `VeranstaltungNeuScreen` |
|
||||
| Turnier Detail | `/veranstaltung/{vId}/turnier/{tId}` | `TurnierDetailScreen` + `NennungsMaske` |
|
||||
| Turnier Neu | `/veranstaltung/{vId}/turnier/neu` | `TurnierNeuScreen` |
|
||||
|
||||
### 🟡 Teilweise implementiert
|
||||
|
||||
| Screen | Route | Offen |
|
||||
|-------------------|----------------------|------------------------------------------------|
|
||||
| Stammdaten Import | `/stammdaten/import` | Status-Polling zum Backend fehlt (ZNS Phase 3) |
|
||||
|
||||
### ⬜ Placeholder (NavRail sichtbar, Screen leer)
|
||||
|
||||
| Screen | Route |
|
||||
|-----------------|--------------------|
|
||||
| Reiter | `/reiter` |
|
||||
| Pferde | `/pferde` |
|
||||
| Funktionäre | `/funktionaere` |
|
||||
| Meisterschaften | `/meisterschaften` |
|
||||
| Cups | `/cups` |
|
||||
|
||||
### 🗑️ Definiert aber nicht in Desktop-Navigation eingebunden
|
||||
|
||||
| Screen | Route | Hinweis |
|
||||
|------------------|----------------------|---------------------------------|
|
||||
| Landing | `/` | Web-App Relikt |
|
||||
| Home | `/home` | Web-App Relikt |
|
||||
| Dashboard | `/dashboard` | Web-App Relikt |
|
||||
| Ping | `/ping` | Dev/Health-Check |
|
||||
| Profile | `/profile` | Web-App Relikt |
|
||||
| OrganizerProfile | `/organizer/profile` | Web-App Relikt |
|
||||
| AuthCallback | `/auth/callback` | Web-App Relikt (OAuth Redirect) |
|
||||
| Nennung | `/nennung` | Web-App Relikt |
|
||||
| CreateTournament | `/tournament/create` | Web-App Relikt |
|
||||
|
||||
---
|
||||
|
||||
## 4. Wichtige Hinweise
|
||||
|
||||
### Auth Guard
|
||||
|
||||
Jeder Screen (außer `Login`) ist durch den Auth Guard geschützt:
|
||||
|
||||
```
|
||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login) {
|
||||
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Veranstaltungen))
|
||||
}
|
||||
```
|
||||
|
||||
### Web-App Relikte im AppScreen
|
||||
|
||||
Es existieren 9 Screens (`Landing`, `Home`, `Dashboard`, `Ping`, `Profile`, `OrganizerProfile`,
|
||||
`AuthCallback`, `Nennung`, `CreateTournament`), die aus der alten Web-App stammen und in der
|
||||
Desktop-App nicht gerendert werden. → **Offene Entscheidung:** Bereinigen oder für zukünftige
|
||||
Web-App-Phase behalten? Siehe ADR-Bedarf.
|
||||
|
||||
### Nennungs-Tab ⭐
|
||||
|
||||
Der `TurnierDetailScreen` enthält den wichtigsten fachlichen Screen: die `NennungsMaske`
|
||||
(Bewerbe-Tab). Dies ist das Herzstück des `registration-context`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Referenzen
|
||||
|
||||
- Quellcode: `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt`
|
||||
- Quellcode: `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/DesktopMainLayout.kt`
|
||||
- Quellcode: `frontend/core/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt`
|
||||
- ZNS-Importer Roadmap: `docs/01_Architecture/Roadmap_ZNS_Importer.md`
|
||||
- Figma Design-Baseline: Vision_03 (ADR Session Log 2026-03-24)
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Session Log: Frontend-Architektur-Refactor
|
||||
|
||||
**Datum:** 26.03.2026
|
||||
**Agent:** 🏗️ Lead Architect
|
||||
**Dauer:** ~1 Session
|
||||
**Trigger:** Inkonsistente Schichttrennung — fachliche Screens lagen im Shell statt in Features
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Die Desktop-App hatte alle Screen-Composables direkt im Shell-Modul (`meldestelle-desktop/screens/`).
|
||||
Das verletzt das Shell-Feature-Core-Pattern:
|
||||
|
||||
- Shell enthielt Fachlogik (Veranstalter, Veranstaltung, Turnier, ZNS)
|
||||
- `zns-import-feature` war gespalten: ViewModel im Feature, Screen im Shell
|
||||
- `SharedUiModels` (Enums, Badges) lagen im Shell statt in `core/design-system`
|
||||
|
||||
---
|
||||
|
||||
## Durchgeführte Änderungen
|
||||
|
||||
### Neue Feature-Module
|
||||
|
||||
| Modul | Inhalt |
|
||||
|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `frontend/features/veranstalter-feature` | VeranstalterAuswahlScreen, VeranstalterDetailScreen, VeranstalterNeuScreen |
|
||||
| `frontend/features/veranstaltung-feature` | AdminUebersichtScreen, VeranstaltungenScreen, VeranstaltungDetailScreen, VeranstaltungNeuScreen, VeranstaltungUebersichtScreen |
|
||||
| `frontend/features/turnier-feature` | TurnierDetailScreen, TurnierNeuScreen, alle 8 Turnier-Tabs, AktorScreens |
|
||||
|
||||
### Verschobene Dateien
|
||||
|
||||
| Von | Nach |
|
||||
|----------------------------------------------------|----------------------------------------------------|
|
||||
| `shells/.../screens/zns/StammdatenImportScreen.kt` | `features/zns-import-feature/.../presentation/` |
|
||||
| `shells/.../screens/shared/SharedUiModels.kt` | `core/design-system/.../models/` |
|
||||
| `shells/.../screens/shared/PlaceholderContent.kt` | `core/design-system/.../models/` |
|
||||
| `shells/.../screens/veranstalter/*.kt` | `features/veranstalter-feature/.../presentation/` |
|
||||
| `shells/.../screens/veranstaltung/*.kt` | `features/veranstaltung-feature/.../presentation/` |
|
||||
| `shells/.../screens/turnier/*.kt` | `features/turnier-feature/.../presentation/` |
|
||||
| `shells/.../screens/aktor/AktorScreens.kt` | `features/turnier-feature/.../presentation/` |
|
||||
|
||||
### Konfiguration
|
||||
|
||||
- `settings.gradle.kts`: 3 neue Feature-Module eingetragen
|
||||
- `shells/meldestelle-desktop/build.gradle.kts`: 3 neue Feature-Abhängigkeiten
|
||||
- `shells/.../DesktopMainLayout.kt`: Imports auf neue Feature-Packages umgestellt
|
||||
- `shells/.../ScreenPreviews.kt`: Imports auf neue Feature-Packages umgestellt
|
||||
|
||||
### Shell nach Refactor
|
||||
|
||||
Der Shell enthält jetzt nur noch:
|
||||
|
||||
- `main.kt` — Koin-Init, Window
|
||||
- `DesktopApp.kt` — Root-Composable, Login-Gate
|
||||
- `di/DesktopModule.kt` — Shell-DI
|
||||
- `navigation/DesktopNavigationPort.kt` — Navigation-Port
|
||||
- `screens/layout/DesktopMainLayout.kt` — Layout + Navigation
|
||||
- `screens/preview/ScreenPreviews.kt` — IDE-Previews
|
||||
|
||||
### Neue Dokumentation
|
||||
|
||||
- `docs/06_Frontend/ARCHITECTURE_RULES.md` — verbindliche Architektur-Richtlinien mit Checkliste und Anti-Patterns
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Gradle-Sync erforderlich, damit Typesafe-Accessors für neue Module generiert werden
|
||||
- `ArchitectureTest` (falls vorhanden) sollte die neuen Schichtgrenzen prüfen
|
||||
|
||||
---
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
Architektur-Regeln müssen **schriftlich und verbindlich** festgehalten werden, bevor Code geschrieben wird.
|
||||
Die `ARCHITECTURE_RULES.md` ist ab sofort Pflichtlektüre für jeden Agenten vor dem ersten Commit.
|
||||
114
docs/99_Journal/2026-03-26_Session_Log_Figma_Konformitaet.md
Normal file
114
docs/99_Journal/2026-03-26_Session_Log_Figma_Konformitaet.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Frontend Expert + UI/UX Designer
|
||||
last_update: 2026-03-26
|
||||
---
|
||||
|
||||
# Session Log: Desktop-App Figma-Konformität (Vision_03)
|
||||
|
||||
🎨 **[Frontend Expert]** / 🖌️ **[UI/UX Designer]** / 🧹 **[Curator]** | 26. März 2026
|
||||
|
||||
## Kontext
|
||||
|
||||
Ziel: Desktop-App an Figma Vision_03 (22 Screenshots) angleichen.
|
||||
Styling hat keine Priorität — Struktur, Layout und Inhalte stehen im Vordergrund.
|
||||
|
||||
---
|
||||
|
||||
## Analyse: Figma Vision_03 (22 Screenshots)
|
||||
|
||||
| Screenshot | Screen / Tab | Status |
|
||||
|----------------|--------------------------------|---------------------|
|
||||
| 01, 04, 05 | TurnierDetail > NENNUNGEN | Struktur vorhanden |
|
||||
| 02 | TurnierDetail > ERGEBNISLISTEN | Struktur vorhanden |
|
||||
| 03 | TurnierDetail > STARTLISTEN | Struktur vorhanden |
|
||||
| 06 | TurnierDetail > ABRECHNUNG | ✅ Neu implementiert |
|
||||
| 07, 08 | TurnierDetail > ARTIKEL | ✅ Neu implementiert |
|
||||
| 09, 10, 11, 12 | TurnierDetail > BEWERBE | Struktur vorhanden |
|
||||
| 13, 14 | TurnierDetail > ORGANISATION | ✅ Neu implementiert |
|
||||
| 15, 16 | TurnierDetail > STAMMDATEN | ✅ Neu implementiert |
|
||||
| 17 | VeranstaltungUebersichtScreen | ✅ Überarbeitet |
|
||||
| 18, 19 | VeranstalterDetailScreen | ✅ Neu implementiert |
|
||||
| 20, 22 | VeranstalterAuswahlScreen | ✅ Neu implementiert |
|
||||
| 21 | Neuer Veranstalter (Formular) | ⬜ TODO |
|
||||
|
||||
---
|
||||
|
||||
## Erledigte Änderungen
|
||||
|
||||
### 1. ✅ SharedUiModels.kt (NEU)
|
||||
|
||||
- Gemeinsame Enums: `LoginStatus`, `VeranstaltungStatus`
|
||||
- Gemeinsame Composable: `LoginStatusBadge`
|
||||
- Eliminiert Duplikate aus 3 Dateien
|
||||
|
||||
### 2. ✅ VeranstalterAuswahlScreen.kt (ÜBERARBEITET)
|
||||
|
||||
- OEPS-Nummer, Ansprechpartner, E-Mail, Login-Status-Badge
|
||||
- "+ Neuer Veranstalter"-Button
|
||||
- Hinweis-Box (blau)
|
||||
- Abbrechen / "Weiter zum Veranstalter"-Buttons unten
|
||||
|
||||
### 3. ✅ VeranstalterDetailScreen.kt (ÜBERARBEITET)
|
||||
|
||||
- Avatar-Circle (Initialen)
|
||||
- OEPS-Nummer, Kontaktdetails-Grid (Ansprechpartner, E-Mail, Telefon, Adresse, Login-Status, Mitglied-seit)
|
||||
- "Profil bearbeiten"-Button
|
||||
- Suchfeld + Status-Filter-Chips (Alle/Vorbereitung/Live/Abgeschlossen)
|
||||
- Veranstaltungs-Liste mit Statistiken (Nennungen, Bewerbe, Letzte Aktivität)
|
||||
|
||||
### 4. ✅ VeranstaltungUebersichtScreen.kt (ÜBERARBEITET)
|
||||
|
||||
- "VERANSTALTUNG - ÜBERSICHT"-Tab-Header
|
||||
- Turnier-Nummer als echte ZNS-Nummer (26128, 26129, ...)
|
||||
- Buttons: Öffnen / Import / Export / USB
|
||||
|
||||
### 5. ✅ TurnierStammdatenTab.kt (NEU)
|
||||
|
||||
- Turnier-Konfiguration: Nr., Typ (OTO/FEI), ZNS-Import via Internet/USB, Sprache
|
||||
- Sparten-Checkboxen (Dressur, Springen), Klassen (C/B/A), Kategorien, Datum
|
||||
- Turnier-Beschreibung: Titel, Sub-Titel
|
||||
- Sponsoren-Sektion
|
||||
|
||||
### 6. ✅ TurnierOrganisationTab.kt (NEU)
|
||||
|
||||
- Funktionäre & Offizielle: Turnierleiter, Turnierbeauftragter, Technischer Delegierter, Parcourschef
|
||||
- Support-Team: Tierarzt, Schmied, Steward
|
||||
- Richterkollegium: dynamische Liste (Name, Qualifikation-Dropdown, Funktion-Dropdown, Löschen)
|
||||
- Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen)
|
||||
|
||||
### 7. ✅ TurnierArtikelTab.kt (NEU)
|
||||
|
||||
- Nennungs- und Startgebühren: Nenngebühr, Startgebühr, Sporteuro, Nachnennungsgebühr, Nennungstausch
|
||||
- Stallungen & Boxen: Box/Tag, Einstreu, Paddock
|
||||
- Zusatzgebühren: dynamische Liste (Bezeichnung, Betrag, Pflicht-Checkbox)
|
||||
- Hinweis-Box zur Preisliste
|
||||
|
||||
### 8. ✅ TurnierAbrechnungTab.kt (NEU)
|
||||
|
||||
- Sub-Tabs: BUCHUNGEN | OFFENE POSTEN | RECHNUNG
|
||||
- Rechte Sidebar: AUSWAHL | VERKAUF | BUCHUNGEN | ADRESSEN
|
||||
- Buchungstabelle: Buchungstext, Soll, Haben, Saldo (rot bei offen), Buchen/Rechnung-Checkboxen
|
||||
- Sidebar: Reiter/Pferd-Suche, Buchen-Betrag, Direkt-Drucken, Zahlungsart (BAR/Scheck/Bankomat/Kreditkarte)
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte (TODO)
|
||||
|
||||
| Prio | Thema | Aufwand |
|
||||
|-------|------------------------------------------------------|---------|
|
||||
| 🟡 P1 | "Neuer Veranstalter"-Formular (Screenshot 21) | Klein |
|
||||
| 🟡 P1 | BEWERBE-Tab: echte Datentabelle + Bewerb-Formular | Mittel |
|
||||
| 🟡 P1 | NENNUNGEN-Tab: Pferd+Reiter-Suche + Bewerbsübersicht | Mittel |
|
||||
| 🟡 P2 | STARTLISTEN-Tab: Bewerbs-Tabs + Sortierung/Zeit | Mittel |
|
||||
| 🟡 P2 | ERGEBNISLISTEN-Tab: Bewerbs-Tabs + Platzierung | Mittel |
|
||||
| 🟢 P3 | Styling: Farben, Fonts, Abstände gemäß Figma | Klein |
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- Figma Screenshots: `docs/06_Frontend/FIGMA/Vision_03/Screenshots/`
|
||||
- Quellcode: `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/`
|
||||
- Routing-Diagramm: `docs/06_Frontend/Navigation_Routing_Diagramm.md`
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# Session Log – Figma-Konformität Teil 2
|
||||
|
||||
**Datum:** 26.03.2026
|
||||
**Agent:** 🎨 Frontend Expert + 🏗️ Lead Architect (Junie)
|
||||
**Dauer:** ~1h
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
Offene Punkte aus Session "Figma-Konformität Teil 1" abarbeiten:
|
||||
|
||||
- "Neuer Veranstalter"-Formular (Screenshot 21)
|
||||
- BEWERBE-Tab: echte Datentabelle + alle 4 Detail-Panel Sub-Tabs
|
||||
- NENNUNGEN-Tab: Pferd+Reiter-Suche + Nennungs-Tabelle + Verkauf/Buchungen
|
||||
- STARTLISTEN-Tab: Bewerbs-Tabs + Sortierung & Zeit-Panel
|
||||
- ERGEBNISLISTEN-Tab: Bewerbs-Tabs + Platzierung & Geldpreis-Panel
|
||||
|
||||
---
|
||||
|
||||
## Umgesetzte Änderungen
|
||||
|
||||
### Neue Dateien
|
||||
|
||||
| Datei | Inhalt |
|
||||
|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `VeranstalterNeuScreen.kt` | Formular gemäß Screenshot 21: Info-Banner, Vereinsdaten, Kontaktdaten, Adresse, Footer-Buttons mit Validierung |
|
||||
| `TurnierBewerbeTab.kt` | Vollständige BEWERBE-Implementierung: Datentabelle (12 Spalten), 4 Sub-Tabs (Bewerb/Bewertung/Geldpreise/Ort/Zeit) mit echten Feldern |
|
||||
| `TurnierNennungenTab.kt` | NENNUNGEN: Pferd+Reiter-Suche, Nennungs-Tabelle mit Status-Badges, Verkauf/Buchungen, Bewerbsübersicht |
|
||||
| `TurnierStartlistenTab.kt` | STARTLISTEN: Bewerbs-Tabs, Starter-Tabelle, Sortierungs-Optionen, Zeiten-Panel |
|
||||
| `TurnierErgebnislistenTab.kt` | ERGEBNISLISTEN: Bewerbs-Tabs, Ergebnis-Tabelle, Platzierung & Geldpreis-Panel |
|
||||
|
||||
### Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|--------------------------------|----------------------------------------------------------------------------|
|
||||
| `AppScreen.kt` | `VeranstalterNeu` als neuer Screen hinzugefügt |
|
||||
| `VeranstalterAuswahlScreen.kt` | `onNeuerVeranstalter`-Parameter + Button verdrahtet |
|
||||
| `DesktopMainLayout.kt` | VeranstalterNeu-Screen registriert + Breadcrumb-Eintrag |
|
||||
| `TurnierDetailScreen.kt` | Placeholder-Implementierungen entfernt, Verweis auf dedizierte Tab-Dateien |
|
||||
|
||||
---
|
||||
|
||||
## Qualitätssicherung
|
||||
|
||||
- Lint-Check auf alle 9 geänderten/neuen Dateien: **keine Fehler**
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte (Phase 4/5)
|
||||
|
||||
- Echte Daten aus Backend laden (alle Tabs zeigen noch Placeholder-Daten)
|
||||
- ZNS-Frontend-Integration (Status-Polling) noch offen
|
||||
- Web-App-Strategie (ADR-0017) noch nicht entschieden
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Session Log: Screens-Reorganisation
|
||||
|
||||
**Datum:** 2026-03-26
|
||||
**Agent:** 🎨 Frontend Expert (Junie)
|
||||
**Scope:** `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/`
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
Ordnung in das `screens/`-Paket bringen: 24 Flat-Dateien in logische Unterordner aufteilen für bessere Übersicht und
|
||||
Wartbarkeit.
|
||||
|
||||
## Neue Paketstruktur
|
||||
|
||||
```
|
||||
screens/
|
||||
├── shared/ → SharedUiModels.kt, PlaceholderContent.kt
|
||||
├── layout/ → DesktopMainLayout.kt
|
||||
├── veranstalter/ → VeranstalterAuswahlScreen.kt, VeranstalterDetailScreen.kt, VeranstalterNeuScreen.kt
|
||||
├── veranstaltung/ → AdminUebersichtScreen.kt, VeranstaltungenScreen.kt, VeranstaltungDetailScreen.kt,
|
||||
│ VeranstaltungNeuScreen.kt, VeranstaltungUebersichtScreen.kt
|
||||
├── turnier/ → TurnierDetailScreen.kt, TurnierNeuScreen.kt,
|
||||
│ TurnierStammdatenTab.kt, TurnierOrganisationTab.kt, TurnierBewerbeTab.kt,
|
||||
│ TurnierArtikelTab.kt, TurnierAbrechnungTab.kt, TurnierNennungenTab.kt,
|
||||
│ TurnierStartlistenTab.kt, TurnierErgebnislistenTab.kt
|
||||
├── zns/ → StammdatenImportScreen.kt
|
||||
├── aktor/ → AktorScreens.kt
|
||||
└── preview/ → ScreenPreviews.kt
|
||||
```
|
||||
|
||||
## Durchgeführte Änderungen
|
||||
|
||||
| Datei | Änderung |
|
||||
|--------------------------------------------------|--------------------------------------------------------------------------------------|
|
||||
| Alle 24 Dateien | Package-Deklaration auf neues Sub-Package angepasst |
|
||||
| `layout/DesktopMainLayout.kt` | Imports für alle Screen-Packages ergänzt |
|
||||
| `preview/ScreenPreviews.kt` | Imports für alle Screen- und Tab-Composables ergänzt |
|
||||
| `veranstalter/VeranstalterAuswahlScreen.kt` | Import `shared.LoginStatus`, `shared.LoginStatusBadge` |
|
||||
| `veranstalter/VeranstalterDetailScreen.kt` | Import `shared.LoginStatus`, `shared.LoginStatusBadge`, `shared.VeranstaltungStatus` |
|
||||
| `veranstaltung/AdminUebersichtScreen.kt` | Import `shared.VeranstaltungStatus` |
|
||||
| `veranstaltung/VeranstaltungUebersichtScreen.kt` | Import `shared.VeranstaltungStatus` |
|
||||
| `DesktopApp.kt` | Import von `screens.DesktopMainLayout` → `screens.layout.DesktopMainLayout` |
|
||||
|
||||
## Verifikation
|
||||
|
||||
- Lint-Check auf alle kritischen Dateien: ✅ keine Fehler
|
||||
- Alte Flat-Dateien gelöscht
|
||||
- `screens/`-Root enthält nur noch die 8 Unterordner
|
||||
78
docs/99_Journal/2026-03-26_Session_Log_Struktur_Sprint.md
Normal file
78
docs/99_Journal/2026-03-26_Session_Log_Struktur_Sprint.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-26
|
||||
---
|
||||
|
||||
# Session Log: Struktur-Sprint & Orientierung
|
||||
|
||||
🏗️ **[Lead Architect]** / 🧹 **[Curator]** | 26. März 2026
|
||||
|
||||
## Kontext
|
||||
|
||||
Nach intensiver Phase-4-Arbeit fehlte der Überblick. Ziel dieser Session: Orientierung
|
||||
wiederherstellen, offene Baustellen priorisieren, zwei konkrete Aufgaben abarbeiten.
|
||||
|
||||
---
|
||||
|
||||
## Erledigte Aufgaben
|
||||
|
||||
### 1. ✅ Routing-Diagramm erstellt
|
||||
|
||||
- **Artefakt:** `docs/06_Frontend/Navigation_Routing_Diagramm.md`
|
||||
- Vollständiges Mermaid-Flowchart aller Screens und Navigationsübergänge
|
||||
- Screen-Status-Tabellen (✅ implementiert / 🟡 teilweise / ⬜ Placeholder / 🗑️ Relikt)
|
||||
- **Fund:** 9 Web-App-Relikte im `AppScreen` (siehe offene Entscheidung unten)
|
||||
|
||||
### 2. ✅ CI/CD Gradle-Version synchronisiert
|
||||
|
||||
- **Datei:** `.gitea/workflows/docker-publish.yaml`
|
||||
- `GRADLE_VERSION` von `9.3.1` auf `9.4.0` korrigiert (synchron mit `gradle-wrapper.properties`)
|
||||
- `paths:`-Whitelist war bereits korrekt — Doku-Änderungen triggern die Pipeline nicht
|
||||
|
||||
---
|
||||
|
||||
## Priorisierte Backlog-Übersicht (Stand 26.03.2026)
|
||||
|
||||
| Prio | Thema | Agent | Status |
|
||||
|-------|--------------------------------------------|--------------------------------|----------------------|
|
||||
| 🔴 P1 | Desktop-App: Figma-Konformität (Vision_03) | 🎨 Frontend Expert + 🖌️ UI/UX | ⬜ Offen |
|
||||
| 🔴 P1 | ZNS-Importer Phase 3: Status-Polling | 🎨 Frontend Expert | ⬜ Offen |
|
||||
| 🟡 P2 | Web-App-Strategie: ADR erforderlich | 🏗️ Lead Architect | ⬜ Entscheidung offen |
|
||||
| 🟡 P2 | Docker & Datenbanken aufräumen | 🐧 DevOps Engineer | ⬜ Offen |
|
||||
| 🟢 P3 | CI/CD weiter optimieren | 🐧 DevOps Engineer | ✅ Teilweise erledigt |
|
||||
|
||||
---
|
||||
|
||||
## Offene Entscheidung: Web-App-Strategie
|
||||
|
||||
Im `AppScreen` existieren 9 Screens, die aus der alten Web-App stammen und in der
|
||||
Desktop-App nicht gerendert werden:
|
||||
`Landing`, `Home`, `Dashboard`, `Ping`, `Profile`, `OrganizerProfile`,
|
||||
`AuthCallback`, `Nennung`, `CreateTournament`
|
||||
|
||||
**Optionen:**
|
||||
|
||||
- **A) Bereinigen:** Relikte aus `AppScreen` entfernen → sauberer Code, weniger Verwirrung
|
||||
- **B) Behalten:** Für zukünftige Web-App-Phase (Phase 7) aufheben → kein Aufwand jetzt
|
||||
- **C) Web-App wieder aufbauen:** `meldestelle-portal` reaktivieren → ADR + Planung nötig
|
||||
|
||||
→ **ADR-0017 erforderlich**, sobald Entscheidung getroffen.
|
||||
|
||||
---
|
||||
|
||||
## Nächste empfohlene Schritte
|
||||
|
||||
1. **Web-App-Entscheidung** treffen (Option A/B/C) → ADR-0017
|
||||
2. **Desktop-App Figma-Delta** aufnehmen: Figma Vision_03 vs. aktueller Compose-Code
|
||||
3. **ZNS Phase 3** abschließen: Status-Polling im `StammdatenImportScreen`
|
||||
4. **Docker/DB** Ist-Zustand prüfen
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- Routing-Diagramm: `docs/06_Frontend/Navigation_Routing_Diagramm.md`
|
||||
- CI/CD Pipeline: `.gitea/workflows/docker-publish.yaml`
|
||||
- MASTER_ROADMAP: `docs/01_Architecture/MASTER_ROADMAP.md`
|
||||
|
|
@ -21,6 +21,7 @@ kotlin {
|
|||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(libs.bundles.kmp.common)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.frontend.core.designsystem.models
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package at.mocode.frontend.core.designsystem.models
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// ── Gemeinsame Enums ─────────────────────────────────────────────────────────
|
||||
enum class LoginStatus { AKTIV, AUSSTEHEND }
|
||||
enum class VeranstaltungStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
|
||||
|
||||
// ── Gemeinsame Composables ───────────────────────────────────────────────────
|
||||
@Composable
|
||||
fun LoginStatusBadge(status: LoginStatus, modifier: Modifier = Modifier) {
|
||||
val (text, color) = when (status) {
|
||||
LoginStatus.AKTIV -> "Aktiv" to Color(0xFF16A34A)
|
||||
LoginStatus.AUSSTEHEND -> "Ausstehend" to Color(0xFFEA580C)
|
||||
}
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
color = color.copy(alpha = 0.15f),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ sealed class AppScreen(val route: String) {
|
|||
|
||||
// Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
||||
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
||||
data object VeranstalterNeu : AppScreen("/veranstalter/neu")
|
||||
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||
data class VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ kotlin {
|
|||
|
||||
jvmMain.dependencies {
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(compose.uiTooling)
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ data class PingUiState(
|
|||
val logs: List<LogEntry> = emptyList()
|
||||
)
|
||||
|
||||
class PingViewModel(
|
||||
open class PingViewModel(
|
||||
private val apiClient: PingApi,
|
||||
private val syncService: PingSyncService
|
||||
) : ViewModel() {
|
||||
|
||||
var uiState by mutableStateOf(PingUiState())
|
||||
private set
|
||||
internal set
|
||||
|
||||
private fun addLog(source: String, message: String, isError: Boolean = false) {
|
||||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingEvent
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.feature.domain.PingSyncService
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Fake-Implementierungen für Preview (kein Koin, kein Netzwerk nötig)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private val fakePingResponse = PingResponse(
|
||||
status = "OK", timestamp = "2026-03-26T12:00:00Z", service = "ping-service"
|
||||
)
|
||||
|
||||
private val fakeEnhancedResponse = EnhancedPingResponse(
|
||||
status = "OK", timestamp = "2026-03-26T12:00:00Z", service = "ping-service",
|
||||
circuitBreakerState = "CLOSED", responseTime = 42L
|
||||
)
|
||||
|
||||
private val fakeHealthResponse = HealthResponse(
|
||||
status = "UP", timestamp = "2026-03-26T12:00:00Z", service = "ping-service", healthy = true
|
||||
)
|
||||
|
||||
private object FakePingApi : PingApi {
|
||||
override suspend fun simplePing() = fakePingResponse
|
||||
override suspend fun enhancedPing(simulate: Boolean) = fakeEnhancedResponse
|
||||
override suspend fun healthCheck() = fakeHealthResponse
|
||||
override suspend fun publicPing() = fakePingResponse
|
||||
override suspend fun securePing() = fakePingResponse
|
||||
override suspend fun syncPings(since: Long): List<PingEvent> = emptyList()
|
||||
}
|
||||
|
||||
private object FakePingSyncService : PingSyncService {
|
||||
override suspend fun syncPings() { /* no-op */
|
||||
}
|
||||
}
|
||||
|
||||
// Subclass um uiState für Preview direkt setzen zu können
|
||||
private class PreviewPingViewModel(state: PingUiState) :
|
||||
PingViewModel(FakePingApi, FakePingSyncService) {
|
||||
init {
|
||||
uiState = state
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Previews
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewPingScreen_Empty() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(PingUiState()),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewPingScreen_WithData() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(
|
||||
PingUiState(
|
||||
simplePingResponse = fakePingResponse,
|
||||
healthResponse = fakeHealthResponse,
|
||||
logs = listOf(
|
||||
LogEntry("12:00:01", "SimplePing", "Success: OK from ping-service"),
|
||||
LogEntry("12:00:00", "HealthCheck", "Status: UP, Healthy: true"),
|
||||
)
|
||||
)
|
||||
),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewPingScreen_Loading() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(PingUiState(isLoading = true, isSyncing = true)),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewPingScreen_Error() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(
|
||||
PingUiState(errorMessage = "Connection refused: Backend nicht erreichbar")
|
||||
),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
32
frontend/features/turnier-feature/build.gradle.kts
Normal file
32
frontend/features/turnier-feature/build.gradle.kts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Feature-Modul: Turnier-Verwaltung (Desktop-only)
|
||||
* Kapselt alle Screens und Tabs für Turnier-Detail, -Neuanlage und alle Turnier-Tabs
|
||||
* (Stammdaten, Organisation, Bewerbe, Artikel, Abrechnung, Nennungen, Startlisten, Ergebnislisten).
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
|
||||
/**
|
||||
* Placeholder-Screens für Akteur-Verwaltung (actor-context).
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Print
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
private val OffenePostenRot = Color(0xFFDC2626)
|
||||
|
||||
/**
|
||||
* ABRECHNUNG-Tab im TurnierDetailScreen.
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_06):
|
||||
* - Sub-Tabs: BUCHUNGEN | OFFENE POSTEN | RECHNUNG
|
||||
* - Rechte Sidebar: AUSWAHL | VERKAUF | BUCHUNGEN | ADRESSEN
|
||||
* - Buchungstabelle: Buchungstext, Soll, Haben, Saldo, Buchen-Checkbox, Rechnung-Checkbox
|
||||
* - Rechte Sidebar: Suche nach Reiter/Pferd, Zahlungsart, Buchen-Button
|
||||
*/
|
||||
@Composable
|
||||
fun AbrechnungTabContent() {
|
||||
var subTab by remember { mutableIntStateOf(0) }
|
||||
var sidebarTab by remember { mutableIntStateOf(2) } // BUCHUNGEN default
|
||||
val subTabs = listOf("BUCHUNGEN", "OFFENE POSTEN", "RECHNUNG")
|
||||
val sidebarTabs = listOf("AUSWAHL", "VERKAUF", "BUCHUNGEN", "ADRESSEN")
|
||||
|
||||
// Placeholder-Buchungen
|
||||
val buchungen = remember {
|
||||
listOf(
|
||||
BuchungspositionUiModel("Startgebühr Bewerb 12 - Dressur Kl. A", 25.00, 0.00),
|
||||
BuchungspositionUiModel("Startgebühr Bewerb 15 - Springen Kl. B", 30.00, 0.00),
|
||||
BuchungspositionUiModel("Nenngeld", 15.00, 0.00),
|
||||
BuchungspositionUiModel("Box 3 Tage", 45.00, 0.00),
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Hauptbereich ─────────────────────────────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
// Sub-Tabs
|
||||
TabRow(
|
||||
selectedTabIndex = subTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
) {
|
||||
subTabs.forEachIndexed { i, title ->
|
||||
Tab(
|
||||
selected = subTab == i,
|
||||
onClick = { subTab = i },
|
||||
text = {
|
||||
Text(
|
||||
title,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (subTab == i) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (subTab) {
|
||||
0 -> BuchungenContent(buchungen)
|
||||
1 -> OffenePostenContent()
|
||||
2 -> RechnungContent()
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
// ── Rechte Sidebar ───────────────────────────────────────────────────
|
||||
Column(modifier = Modifier.width(320.dp).fillMaxHeight()) {
|
||||
TabRow(
|
||||
selectedTabIndex = sidebarTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
) {
|
||||
sidebarTabs.forEachIndexed { i, title ->
|
||||
Tab(
|
||||
selected = sidebarTab == i,
|
||||
onClick = { sidebarTab = i },
|
||||
text = { Text(title, fontSize = 11.sp) },
|
||||
)
|
||||
}
|
||||
}
|
||||
when (sidebarTab) {
|
||||
2 -> BuchungenSidebar()
|
||||
else -> PlaceholderContent(title = sidebarTabs[sidebarTab], subtitle = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BuchungenContent(buchungen: List<BuchungspositionUiModel>) {
|
||||
val gesamtSoll = buchungen.sumOf { it.soll }
|
||||
val gesamtHaben = buchungen.sumOf { it.haben }
|
||||
val gesamtSaldo = gesamtSoll - gesamtHaben
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Aktualisieren", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
|
||||
Text("Übersicht", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
|
||||
Text("Tabelle Leeren", fontSize = 12.sp, color = Color(0xFFEA580C))
|
||||
}
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
|
||||
Text("Pferd aus Liste entfernen", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Tabellen-Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(Color(0xFFF3F4F6)).padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text("Buchungstext", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
|
||||
Text("Soll", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Haben", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Saldo", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Buchen", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
|
||||
Text("Rechnung", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
items(buchungen) { b ->
|
||||
val saldo = b.soll - b.haben
|
||||
var buchen by remember { mutableStateOf(false) }
|
||||
var rechnung by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(b.buchungstext, fontSize = 13.sp, modifier = Modifier.weight(3f))
|
||||
Text("%.2f €".format(b.soll), fontSize = 13.sp, modifier = Modifier.weight(1f))
|
||||
Text("%.2f €".format(b.haben), fontSize = 13.sp, modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
"%.2f €".format(saldo),
|
||||
fontSize = 13.sp,
|
||||
color = if (saldo > 0) OffenePostenRot else Color.Unspecified,
|
||||
fontWeight = if (saldo > 0) FontWeight.SemiBold else FontWeight.Normal,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Checkbox(checked = buchen, onCheckedChange = { buchen = it }, modifier = Modifier.width(60.dp))
|
||||
Checkbox(checked = rechnung, onCheckedChange = { rechnung = it }, modifier = Modifier.width(70.dp))
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
}
|
||||
|
||||
// Gesamt-Zeile
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("GESAMT", fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(3f))
|
||||
Text("%.2f €".format(gesamtSoll), fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Text("%.2f €".format(gesamtHaben), fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
"%.2f €".format(gesamtSaldo),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (gesamtSaldo > 0) OffenePostenRot else Color.Unspecified,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BuchungenSidebar() {
|
||||
var suchtext by remember { mutableStateOf("") }
|
||||
var zahlungsart by remember { mutableStateOf("BAR") }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Nach Reiter oder Pferd", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
OutlinedTextField(
|
||||
value = suchtext,
|
||||
onValueChange = { suchtext = it },
|
||||
placeholder = { Text("Bitte auswählen...", fontSize = 12.sp) },
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Buchen:", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("0.00 €", fontSize = 22.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
Button(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) { Text("Buchen") }
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Direkt Drucken:", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) {
|
||||
Icon(Icons.Default.Print, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Saldo", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) {
|
||||
Text("Rechnung", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Zahlungsart:", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
listOf("BAR", "Scheck (+30 €)", "Bankomat", "Kreditkarte").forEach { art ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = zahlungsart == art, onClick = { zahlungsart = art })
|
||||
Text(art, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) { Text("Gebühr buchen") }
|
||||
|
||||
// Hinweis
|
||||
Surface(
|
||||
color = Color(0xFFEFF6FF),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFBFDBFE)),
|
||||
) {
|
||||
Text(
|
||||
"💡 Hinweis: Bei Barzahlung werden die Buchungen sofort verarbeitet. Scheck-Zahlungen erfordern eine zusätzliche Gebühr von 30 €.",
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFF1E40AF),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OffenePostenContent() {
|
||||
PlaceholderContent(title = "Offene Posten", subtitle = "Alle offenen Forderungen …")
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RechnungContent() {
|
||||
PlaceholderContent(title = "Rechnung", subtitle = "Rechnungserstellung …")
|
||||
}
|
||||
|
||||
// --- UI-Modelle ---
|
||||
data class BuchungspositionUiModel(val buchungstext: String, val soll: Double, val haben: Double)
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
private val DeleteRed = Color(0xFFDC2626)
|
||||
|
||||
/**
|
||||
* ARTIKEL-Tab im TurnierDetailScreen.
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_07 / figma-entwurf_08):
|
||||
* - Nennungen & Gebühren: Nenngebühr, Startgebühr, Sporteuro, Nachnennungsgebühr, Nennungstausch
|
||||
* - Stallungen & Boxen: Box/Tag, Einstreu, Paddock
|
||||
* - Zusatzgebühren: dynamische Liste (Bezeichnung, Betrag, Pflicht)
|
||||
*/
|
||||
@Composable
|
||||
fun ArtikelTabContent() {
|
||||
var nenngebuehr by remember { mutableStateOf("0.00") }
|
||||
var startgebuehr by remember { mutableStateOf("15.00") }
|
||||
var sporteuro by remember { mutableStateOf("0.00") }
|
||||
var nachnennungsgebuehr by remember { mutableStateOf("0.00") }
|
||||
var nennungstauschGebuehr by remember { mutableStateOf("0.00") }
|
||||
var boxProTag by remember { mutableStateOf("0.00") }
|
||||
var einstreuErstEinstreu by remember { mutableStateOf("0.00") }
|
||||
var einstreuNachlegen by remember { mutableStateOf("0.00") }
|
||||
var paddockProTag by remember { mutableStateOf("0.00") }
|
||||
|
||||
var zusatzgebuehren by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
ZusatzgebuehrUiModel("Stromanschluss pro Tag", "5.00", false),
|
||||
ZusatzgebuehrUiModel("Camping pro Nacht", "10.00", false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// ── Nennungen & Gebühren ─────────────────────────────────────────────
|
||||
ArtikelSectionCard(title = "Nennungen & Gebühren") {
|
||||
ArtikelSubSection("Nennungs- und Startgebühren") {
|
||||
ArtikelFormRow("Nenngebühr pro Pferd/Reiter:", "(Grundgebühr unabhängig von Anzahl Bewerben)") {
|
||||
EuroTextField(nenngebuehr) { nenngebuehr = it }
|
||||
}
|
||||
ArtikelFormRow("Startgebühr pro Bewerb:", "(Pro einzelner Prüfung)") {
|
||||
EuroTextField(startgebuehr) { startgebuehr = it }
|
||||
}
|
||||
ArtikelFormRow("Sporteuro (Beitrag OEPS):", null) {
|
||||
EuroTextField(sporteuro) { sporteuro = it }
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
ArtikelFormRow("Nachnennungsgebühr:", "(Nach Nennschluss)") {
|
||||
EuroTextField(nachnennungsgebuehr) { nachnennungsgebuehr = it }
|
||||
}
|
||||
ArtikelFormRow("Nennungstausch-Gebühr:", "(Pferd- oder Reiter-Wechsel)") {
|
||||
EuroTextField(nennungstauschGebuehr) { nennungstauschGebuehr = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stallungen & Boxen ───────────────────────────────────────────────
|
||||
ArtikelSectionCard(title = "Stallungen & Boxen") {
|
||||
ArtikelFormRow("Box pro Tag:", null) {
|
||||
EuroTextField(boxProTag) { boxProTag = it }
|
||||
}
|
||||
ArtikelFormRow("Einstreu (Erst-Einstreu):", null) {
|
||||
EuroTextField(einstreuErstEinstreu) { einstreuErstEinstreu = it }
|
||||
}
|
||||
ArtikelFormRow("Einstreu (Nachlegen):", null) {
|
||||
EuroTextField(einstreuNachlegen) { einstreuNachlegen = it }
|
||||
}
|
||||
ArtikelFormRow("Paddock pro Tag:", null) {
|
||||
EuroTextField(paddockProTag) { paddockProTag = it }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zusatzgebühren ───────────────────────────────────────────────────
|
||||
ArtikelSectionCard(
|
||||
title = "Zusatzgebühren",
|
||||
action = {
|
||||
TextButton(onClick = {
|
||||
zusatzgebuehren = zusatzgebuehren + ZusatzgebuehrUiModel("", "0.00", false)
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp), tint = AccentBlue)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Hinzufügen", color = AccentBlue, fontSize = 13.sp)
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Tabellen-Header
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
|
||||
Text("Bezeichnung", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
|
||||
Text("Betrag", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
|
||||
Text("Pflicht", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Spacer(Modifier.width(44.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
zusatzgebuehren.forEachIndexed { idx, z ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = z.bezeichnung,
|
||||
onValueChange = { v ->
|
||||
zusatzgebuehren = zusatzgebuehren.toMutableList().also { it[idx] = z.copy(bezeichnung = v) }
|
||||
},
|
||||
modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = z.betrag,
|
||||
onValueChange = { v ->
|
||||
zusatzgebuehren = zusatzgebuehren.toMutableList().also { it[idx] = z.copy(betrag = v) }
|
||||
},
|
||||
suffix = { Text("€") },
|
||||
modifier = Modifier.weight(1.5f).height(44.dp).padding(end = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = z.pflicht,
|
||||
onCheckedChange = { v ->
|
||||
zusatzgebuehren = zusatzgebuehren.toMutableList().also { it[idx] = z.copy(pflicht = v) }
|
||||
},
|
||||
)
|
||||
Text("Pflicht", fontSize = 12.sp)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { zusatzgebuehren = zusatzgebuehren.toMutableList().also { it.removeAt(idx) } },
|
||||
modifier = Modifier.size(44.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Löschen",
|
||||
tint = DeleteRed,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hinweis ──────────────────────────────────────────────────────────
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color(0xFFFFFBEB),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFFDE68A)),
|
||||
) {
|
||||
Row(modifier = Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("ℹ️", fontSize = 14.sp)
|
||||
Column {
|
||||
Text("Hinweis zur Preisliste", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(
|
||||
"Die Gebührenstruktur wird in der offiziellen Ausschreibung veröffentlicht und ist für alle Teilnehmer verbindlich. " +
|
||||
"Bei nationalen Turnieren der Kategorie C-Neu sind oft reduzierte Gebühren oder Gebührenbefreiungen üblich (z.B. kein Nenngeld, kein Sporteuro).",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF92400E),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aktions-Buttons ──────────────────────────────────────────────────
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
OutlinedButton(onClick = {}) { Text("Zurücksetzen") }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(onClick = {}, colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)) { Text("Speichern") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtikelSectionCard(
|
||||
title: String,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
||||
action?.invoke()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtikelSubSection(title: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9FAFB)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(title, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtikelFormRow(label: String, hint: String?, content: @Composable RowScope.() -> Unit) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label, fontSize = 13.sp, modifier = Modifier.width(220.dp), color = Color(0xFF374151))
|
||||
content()
|
||||
if (hint != null) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
hint,
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFF9CA3AF),
|
||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EuroTextField(value: String, onValueChange: (String) -> Unit) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
suffix = { Text("€") },
|
||||
modifier = Modifier.width(120.dp).height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
// --- UI-Modelle ---
|
||||
data class ZusatzgebuehrUiModel(val bezeichnung: String, val betrag: String, val pflicht: Boolean)
|
||||
|
|
@ -0,0 +1,644 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val HeaderBg = Color(0xFFF1F5F9)
|
||||
private val SelectedRowBg = Color(0xFFEFF6FF)
|
||||
|
||||
/**
|
||||
* BEWERBE-Tab gemäß Vision_03 (Screenshots 09–12).
|
||||
*
|
||||
* Layout: 3-spaltig
|
||||
* - Links (140dp): Aktions-Buttons (Speichern, Rückgängig, Einfügen, Löschen, Teilen, Verschieben, Startliste, Ergebnisliste)
|
||||
* - Mitte (flex): Datentabelle (Tag | Platz | Bewerb | Beginn | Ende | Bewerbname | ZNS | Nennungen)
|
||||
* - Rechts (340dp): Detail-Panel mit Sub-Tabs (Bewerb | Bewertung | Geldpreise | Ort/Zeit)
|
||||
*/
|
||||
@Composable
|
||||
fun BewerbeTabContent() {
|
||||
var selectedIndex by remember { mutableIntStateOf(0) }
|
||||
val bewerbe = remember { sampleBewerbe() }
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Linke Aktions-Spalte ──────────────────────────────────────────────
|
||||
BewerbeAktionsSpalte(modifier = Modifier.width(140.dp).fillMaxHeight())
|
||||
VerticalDivider()
|
||||
|
||||
// ── Mittlere Tabelle ──────────────────────────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
// Toolbar über der Tabelle
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Aktualisieren", fontSize = 12.sp)
|
||||
}
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = PrimaryBlue,
|
||||
) {
|
||||
Text(
|
||||
text = "${bewerbe.size} Bewerbe",
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
|
||||
) {
|
||||
Text("Filtern", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Tabellen-Header
|
||||
BewerbeTableHeader()
|
||||
HorizontalDivider()
|
||||
|
||||
// Tabellen-Zeilen
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(bewerbe) { index, bewerb ->
|
||||
BewerbeTableRow(
|
||||
bewerb = bewerb,
|
||||
isSelected = index == selectedIndex,
|
||||
onClick = { selectedIndex = index },
|
||||
)
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
// ── Rechtes Detail-Panel ──────────────────────────────────────────────
|
||||
BewerbeDetailPanel(
|
||||
bewerb = bewerbe.getOrNull(selectedIndex),
|
||||
modifier = Modifier.width(340.dp).fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeTableHeader() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(HeaderBg)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TableHeaderCell("Tag", 90.dp)
|
||||
TableHeaderCell("Platz", 50.dp)
|
||||
TableHeaderCell("Bewerb", 55.dp)
|
||||
TableHeaderCell("Beginn", 55.dp)
|
||||
TableHeaderCell("Ende", 55.dp)
|
||||
TableHeaderCell("Bewerbname", weight = 1f)
|
||||
TableHeaderCell("ZNS", 45.dp)
|
||||
TableHeaderCell("Nennungen", 75.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TableHeaderCell(text: String, width: androidx.compose.ui.unit.Dp? = null, weight: Float? = null) {
|
||||
val mod = when {
|
||||
weight != null -> Modifier.weight(weight)
|
||||
width != null -> Modifier.width(width)
|
||||
else -> Modifier
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF374151),
|
||||
modifier = mod,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeTableRow(bewerb: BewerbUiModel, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (isSelected) SelectedRowBg else Color.Transparent)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 8.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(bewerb.tag, fontSize = 12.sp, modifier = Modifier.width(90.dp))
|
||||
Text("${bewerb.platz}", fontSize = 12.sp, modifier = Modifier.width(50.dp))
|
||||
Text(
|
||||
"${bewerb.nummer}",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
modifier = Modifier.width(55.dp),
|
||||
color = if (isSelected) PrimaryBlue else Color.Unspecified
|
||||
)
|
||||
Text(bewerb.beginn, fontSize = 12.sp, modifier = Modifier.width(55.dp))
|
||||
Text(bewerb.ende, fontSize = 12.sp, modifier = Modifier.width(55.dp))
|
||||
Text(bewerb.name, fontSize = 12.sp, modifier = Modifier.weight(1f), maxLines = 2)
|
||||
Text("${bewerb.zns}", fontSize = 12.sp, modifier = Modifier.width(45.dp))
|
||||
Text("${bewerb.nennungen}", fontSize = 12.sp, modifier = Modifier.width(75.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeAktionsSpalte(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
AktionsBtn("Änderungen\nSpeichern")
|
||||
AktionsBtn("Änderungen\nRückgängig")
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AktionsBtn("Bewerb\nEinfügen")
|
||||
AktionsBtn("Bewerb\nLöschen")
|
||||
AktionsBtn("Bewerb Teilen")
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AktionsBtn("Bewerb nach\noben verschieben")
|
||||
AktionsBtn("Bewerb nach\nunten verschieben")
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AktionsBtn("Startliste\nBearbeiten")
|
||||
AktionsBtn("Startliste\nDrucken")
|
||||
AktionsBtn("Ergebnisliste\nBearbeiten")
|
||||
AktionsBtn("Ergebnisliste\nDrucken")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AktionsBtn(label: String) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 2.dp),
|
||||
) {
|
||||
Text(label, fontSize = 11.sp, lineHeight = 13.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeDetailPanel(bewerb: BewerbUiModel?, modifier: Modifier = Modifier) {
|
||||
var subTab by remember { mutableIntStateOf(0) }
|
||||
val subTabs = listOf("Bewerb", "Bewertung", "Geldpreise", "Ort/Zeit")
|
||||
|
||||
Column(modifier = modifier) {
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = subTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = PrimaryBlue,
|
||||
) {
|
||||
subTabs.forEachIndexed { i, title ->
|
||||
Tab(
|
||||
selected = subTab == i,
|
||||
onClick = { subTab = i },
|
||||
text = { Text(title, fontSize = 12.sp) },
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
when (subTab) {
|
||||
0 -> BewerbSubTab(bewerb)
|
||||
1 -> BewertungSubTab(bewerb)
|
||||
2 -> GeldpreiseSubTab()
|
||||
3 -> OrtZeitSubTab(bewerb)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sub-Tab: Bewerb ───────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun BewerbSubTab(bewerb: BewerbUiModel?) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
DetailField("Nummer:", bewerb?.nummer?.toString() ?: "")
|
||||
DetailField("Abteilung:", "")
|
||||
DetailField("Typ:", bewerb?.typ ?: "")
|
||||
DetailField("Name:", bewerb?.name ?: "")
|
||||
DetailField("Bezeichnung:", bewerb?.bezeichnung ?: "")
|
||||
DetailDropdown("Kategorie:")
|
||||
DetailDropdown("Klasse:")
|
||||
DetailDropdown("Lizenz:")
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Maximal:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
OutlinedTextField(
|
||||
value = "3",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(60.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Pferde je Reiter", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
DetailDropdown("Pferdealter:")
|
||||
DetailField("Zeile 1:", bewerb?.zeile1 ?: "")
|
||||
DetailField("Zeile 2:", "")
|
||||
DetailField("Zeile 3:", "")
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Logo Bewerb:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(40.dp),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
Text("…", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sub-Tab: Bewertung ────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun BewertungSubTab(bewerb: BewerbUiModel?) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text("Bewertungs-Konfiguration", fontWeight = FontWeight.SemiBold, fontSize = 13.sp, color = Color(0xFF374151))
|
||||
DetailField("Prüfung:", "Dressurreiterprüfung")
|
||||
DetailField("Richtverfahren:", "A")
|
||||
DetailField("Para-Grade:", "")
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Richteranzahl:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
OutlinedTextField(
|
||||
value = "2",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(60.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
}
|
||||
DetailField("Aufgabe:", "Aufgabe R")
|
||||
DetailField("Aufgabennummer:", "")
|
||||
DetailField("Maximalpunkte:", "")
|
||||
HorizontalDivider()
|
||||
Text("Richter", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
RichterRow("C:", "Schuster Alexandra")
|
||||
RichterRow("C:", "Vankova Kamila (CZ)")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RichterRow(position: String, name: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(position, fontSize = 13.sp, modifier = Modifier.width(30.dp))
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
Checkbox(checked = true, onCheckedChange = {})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sub-Tab: Geldpreise ───────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun GeldpreiseSubTab() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
// Geldpreis-Sektion
|
||||
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Geldpreis", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = false, onCheckedChange = {})
|
||||
Text("Geldpreis", fontSize = 13.sp)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Startgeld:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
OutlinedTextField(
|
||||
value = "15,00",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(100.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Auszahlung:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
DetailDropdown("fortführend", modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Geldpreis für Kadererreiter
|
||||
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Geldpreis für Kadererreiter", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = false, onCheckedChange = {})
|
||||
Text("Geldpreis für Kadererreiter", fontSize = 13.sp)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Startgeld für Kadererreiter:", fontSize = 13.sp, modifier = Modifier.width(180.dp))
|
||||
OutlinedTextField(
|
||||
value = "15,00",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(100.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Geldpreisvorlage
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Geldpreisvorlage wählen:", fontSize = 13.sp, modifier = Modifier.width(180.dp))
|
||||
DetailDropdown("", modifier = Modifier.weight(1f))
|
||||
}
|
||||
// Tabelle
|
||||
Text("0 Geldpreise", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(HeaderBg).padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text("Nummer", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Geldpreis", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
}
|
||||
HorizontalDivider()
|
||||
Text("•", fontSize = 12.sp, color = Color(0xFF9CA3AF), modifier = Modifier.padding(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sub-Tab: Ort/Zeit ─────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun OrtZeitSubTab(bewerb: BewerbUiModel?) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Tag:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
DetailDropdown(bewerb?.tag ?: "28.05.2023", modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Beginnzeit:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
DetailDropdown("fix um", modifier = Modifier.width(100.dp))
|
||||
}
|
||||
LabeledTimeField("Beginnzeit:", bewerb?.beginn ?: "08:00", "(hh:mm)")
|
||||
LabeledTimeField("Reitdauer:", "02:00", "(mm:ss)")
|
||||
LabeledTimeField("Umbau:", "10", "(mm)")
|
||||
LabeledTimeField("Besichtigung:", "10", "(mm)")
|
||||
LabeledTimeField("Stechen:", "", "(mm)")
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Platz:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
DetailDropdown("Vorderer Turnierplatz", modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabeledTimeField(label: String, value: String, unit: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label, fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(80.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(unit, fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfs-Composables ─────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun DetailField(label: String, value: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label, fontSize = 13.sp, modifier = Modifier.width(130.dp))
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailDropdown(placeholder: String, modifier: Modifier = Modifier) {
|
||||
OutlinedTextField(
|
||||
value = placeholder,
|
||||
onValueChange = {},
|
||||
modifier = modifier,
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
|
||||
trailingIcon = {
|
||||
Text("▼", fontSize = 10.sp, color = Color(0xFF6B7280))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ── UI-Modell ─────────────────────────────────────────────────────────────────
|
||||
|
||||
data class BewerbUiModel(
|
||||
val tag: String,
|
||||
val platz: Int,
|
||||
val nummer: Int,
|
||||
val beginn: String,
|
||||
val ende: String,
|
||||
val name: String,
|
||||
val bezeichnung: String,
|
||||
val typ: String,
|
||||
val zeile1: String,
|
||||
val zns: Int,
|
||||
val nennungen: Int,
|
||||
)
|
||||
|
||||
private fun sampleBewerbe() = listOf(
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
1,
|
||||
"08:00",
|
||||
"08:00",
|
||||
"Dressurreiterprüfung Reiterpass\n(Aufgabe R 1)\nPony Einsteiger Cup OO",
|
||||
"Dressurreiterprüfung Reiterpass",
|
||||
"Dressur",
|
||||
"Pony Einsteiger Cup OO",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
2,
|
||||
"08:20",
|
||||
"08:20",
|
||||
"Dressurreiterprüfung Reitenadel\n(Aufgabe R 4)\nPony Einsteiger Cup OO",
|
||||
"Dressurreiterprüfung Reitenadel",
|
||||
"Dressur",
|
||||
"Pony Einsteiger Cup OO",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
3,
|
||||
"08:40",
|
||||
"08:40",
|
||||
"Dressurreiterprüfung lsf. (Istzfrei)\n(Aufgabe LF 1)",
|
||||
"Dressurreiterprüfung lsf.",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
4,
|
||||
"09:00",
|
||||
"09:00",
|
||||
"Dressurreiterprüfung lsf. (Lizenzfrei)\n(Aufgabe LF 3)",
|
||||
"Dressurreiterprüfung lsf.",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
5,
|
||||
"09:20",
|
||||
"09:20",
|
||||
"Führzügelklasse\nOO Kids Cup",
|
||||
"Führzügelklasse",
|
||||
"Dressur",
|
||||
"OO Kids Cup",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
6,
|
||||
"09:40",
|
||||
"09:40",
|
||||
"First Ridden\nOO Kids Cup",
|
||||
"First Ridden",
|
||||
"Dressur",
|
||||
"OO Kids Cup",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
7,
|
||||
"10:00",
|
||||
"10:00",
|
||||
"Pony Dressurprüfung Kl. A (Aufgabe P 1)",
|
||||
"Pony Dressurprüfung Kl. A",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
8,
|
||||
"10:20",
|
||||
"10:20",
|
||||
"Dressurreiterprüfung Kl. A (Aufgabe DRA 1)",
|
||||
"Dressurreiterprüfung Kl. A",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
9,
|
||||
"10:40",
|
||||
"10:40",
|
||||
"Dressurreiterprüfung Kl. A (Aufgabe A 5)",
|
||||
"Dressurreiterprüfung Kl. A",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
10,
|
||||
"11:00",
|
||||
"11:00",
|
||||
"Pony Dressurprüfung Kl. A (Aufgabe P 9)",
|
||||
"Pony Dressurprüfung Kl. A",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
11,
|
||||
"11:20",
|
||||
"11:20",
|
||||
"Dressurreiterprüfung Kl. L (Aufgabe DRL 1)",
|
||||
"Dressurreiterprüfung Kl. L",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
BewerbUiModel(
|
||||
"28.05.2023",
|
||||
1,
|
||||
12,
|
||||
"11:40",
|
||||
"11:40",
|
||||
"Dressurprüfung Kl. L (Aufgabe L 3)",
|
||||
"Dressurprüfung Kl. L",
|
||||
"Dressur",
|
||||
"",
|
||||
0,
|
||||
0
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Detailansicht eines Turniers gemäß Vision_03.
|
||||
*
|
||||
* Layout: Horizontale Tab-Bar mit 8 Tabs (kein eigener Toolbar-Zurück-Button –
|
||||
* Navigation erfolgt über den Breadcrumb in der TopBar).
|
||||
*
|
||||
* Tabs:
|
||||
* 1. STAMMDATEN – Turnier-Konfiguration, ZNS-Import, Sparten, Datum
|
||||
* 2. ORGANISATION – Funktionäre, Richterkollegium, Austragungsplätze
|
||||
* 3. BEWERBE – 3-spaltiges Layout (Aktionen | Tabelle | Detail-Panel)
|
||||
* 4. ARTIKEL – Gebühren, Stallungen & Boxen, Zusatzgebühren
|
||||
* 5. ABRECHNUNG – Buchungen, Offene Posten, Rechnung
|
||||
* 6. NENNUNGEN – Pferd+Reiter-Suche, Verkauf/Buchungen, Bewerbsübersicht
|
||||
* 7. STARTLISTEN – Bewerbs-Tabs, Sortierung, Zeit/Dauer
|
||||
* 8. ERGEBNISLISTEN – Bewerbs-Tabs, Platzierung & Geldpreise
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
fun TurnierDetailScreen(
|
||||
veranstaltungId: Long,
|
||||
turnierId: Long,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
|
||||
val tabs = listOf(
|
||||
"STAMMDATEN",
|
||||
"ORGANISATION",
|
||||
"BEWERBE",
|
||||
"ARTIKEL",
|
||||
"ABRECHNUNG",
|
||||
"NENNUNGEN",
|
||||
"STARTLISTEN",
|
||||
"ERGEBNISLISTEN",
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Horizontale Tab-Bar (direkt unter der TopBar)
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
edgePadding = 0.dp,
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Tab-Inhalte
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
when (selectedTab) {
|
||||
0 -> StammdatenTabContent(turnierId = turnierId)
|
||||
1 -> OrganisationTabContent()
|
||||
2 -> BewerbeTabContent()
|
||||
3 -> ArtikelTabContent()
|
||||
4 -> AbrechnungTabContent()
|
||||
5 -> NennungenTabContent()
|
||||
6 -> StartlistenTabContent()
|
||||
7 -> ErgebnislistenTabContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab-Inhalte werden in dedizierten Dateien implementiert:
|
||||
// TurnierBewerbeTab.kt → BewerbeTabContent()
|
||||
// TurnierNennungenTab.kt → NennungenTabContent()
|
||||
// TurnierStartlistenTab.kt → StartlistenTabContent()
|
||||
// TurnierErgebnislistenTab.kt → ErgebnislistenTabContent()
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val ElBlue = Color(0xFF1E3A8A)
|
||||
private val ElHeaderBg = Color(0xFFF1F5F9)
|
||||
|
||||
/**
|
||||
* ERGEBNISLISTEN-Tab gemäß Vision_03.
|
||||
*
|
||||
* Layout: 2-spaltig
|
||||
* - Links (flex): Bewerbs-Tabs + Ergebnis-Tabelle (Platz | Startnr | Pferd | Reiter | Fehler | Zeit | Punkte)
|
||||
* - Rechts (280dp): Platzierung & Geldpreis-Panel
|
||||
*/
|
||||
@Composable
|
||||
fun ErgebnislistenTabContent() {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Linke Spalte: Bewerbs-Tabs + Tabelle ─────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
ErgebnislistenBewerbsTabs()
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
// ── Rechte Spalte: Platzierung & Geldpreis ───────────────────────────
|
||||
PlatzierungGeldpreisPanel(modifier = Modifier.width(280.dp).fillMaxHeight())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErgebnislistenBewerbsTabs() {
|
||||
val bewerbe = remember {
|
||||
listOf("Bewerb 1", "Bewerb 2", "Bewerb 3", "Bewerb 4", "Bewerb 5")
|
||||
}
|
||||
var selectedBewerb by remember { mutableIntStateOf(0) }
|
||||
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedBewerb,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = ElBlue,
|
||||
edgePadding = 0.dp,
|
||||
) {
|
||||
bewerbe.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedBewerb == index,
|
||||
onClick = { selectedBewerb = index },
|
||||
text = { Text(title, fontSize = 12.sp) },
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Bewerb ${selectedBewerb + 1} – Ergebnisliste",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp)
|
||||
) {
|
||||
Text("Importieren", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp)
|
||||
) {
|
||||
Text("Exportieren", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp)
|
||||
) {
|
||||
Text("Drucken", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Tabellen-Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(ElHeaderBg).padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text("Platz", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(50.dp))
|
||||
Text("Startnr.", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(65.dp))
|
||||
Text("Pferd", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Reiter", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Fehler", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
|
||||
Text("Zeit", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
|
||||
Text("Punkte", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
// Leere Liste
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Keine Ergebnisse vorhanden", fontSize = 14.sp, color = Color(0xFF6B7280))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Ergebnisse werden nach dem Turnier eingetragen.", fontSize = 12.sp, color = Color(0xFF9CA3AF))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = {}) {
|
||||
Text("Ergebnisse importieren", fontSize = 13.sp)
|
||||
}
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = ElBlue),
|
||||
) {
|
||||
Text("Ergebnisse eingeben", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlatzierungGeldpreisPanel(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Platzierung & Geldpreis", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
HorizontalDivider()
|
||||
|
||||
// Anzahl Platzierte
|
||||
Text("Platzierung", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Anzahl Platzierte:", fontSize = 12.sp, modifier = Modifier.width(140.dp))
|
||||
OutlinedTextField(
|
||||
value = "3",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(60.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Stechen ab Platz:", fontSize = 12.sp, modifier = Modifier.width(140.dp))
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(60.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Geldpreise
|
||||
Text("Geldpreise", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(ElHeaderBg).padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text("Platz", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(50.dp))
|
||||
Text("Betrag (€)", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
}
|
||||
HorizontalDivider()
|
||||
listOf(1 to "–", 2 to "–", 3 to "–").forEach { (platz, betrag) ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
"$platz.",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ElBlue,
|
||||
modifier = Modifier.width(50.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = betrag,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.weight(1f).height(36.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
|
||||
)
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Aktions-Buttons
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = ElBlue),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Platzierung berechnen", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Ergebnisliste drucken", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Geldpreise auszahlen", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val NennBlue = Color(0xFF1E3A8A)
|
||||
private val NennHeaderBg = Color(0xFFF1F5F9)
|
||||
private val NennSelectedBg = Color(0xFFEFF6FF)
|
||||
|
||||
/**
|
||||
* NENNUNGEN-Tab gemäß Vision_03.
|
||||
*
|
||||
* Layout: 2-spaltig
|
||||
* - Links (flex): Pferd+Reiter-Suche + Nennungs-Tabelle
|
||||
* - Rechts (360dp): Verkauf/Buchungen + Bewerbsübersicht
|
||||
*/
|
||||
@Composable
|
||||
fun NennungenTabContent() {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Linke Spalte: Suche + Tabelle ─────────────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
NennungenSuchePanel()
|
||||
HorizontalDivider()
|
||||
NennungenTabelle()
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
// ── Rechte Spalte: Verkauf + Bewerbsübersicht ─────────────────────────
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(360.dp)
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
VerkaufBuchungenPanel()
|
||||
HorizontalDivider()
|
||||
BewerbsuebersichtPanel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NennungenSuchePanel() {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Pferd & Reiter suchen", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Pferd suchen (Name, OEPS-Nr.)…", fontSize = 12.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(16.dp)) },
|
||||
modifier = Modifier.weight(1f).height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Reiter suchen (Name, OEPS-Nr.)…", fontSize = 12.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(16.dp)) },
|
||||
modifier = Modifier.weight(1f).height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = NennBlue),
|
||||
modifier = Modifier.height(44.dp),
|
||||
) {
|
||||
Text("Suchen", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NennungenTabelle() {
|
||||
val nennungen = remember { sampleNennungen() }
|
||||
var selectedIndex by remember { mutableIntStateOf(-1) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(NennHeaderBg)
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text("Startnr.", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
|
||||
Text("Pferd", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Reiter", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Bewerb", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Status", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(80.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (nennungen.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Keine Nennungen vorhanden", fontSize = 14.sp, color = Color(0xFF6B7280))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Suchen Sie nach Pferd und Reiter, um eine Nennung hinzuzufügen.",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF9CA3AF)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(nennungen) { index, nennung ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (index == selectedIndex) NennSelectedBg else Color.Transparent)
|
||||
.clickable { selectedIndex = index }
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
"${nennung.startnr}",
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.width(60.dp),
|
||||
color = NennBlue,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(nennung.pferd, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
Text(nennung.reiter, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
Text(nennung.bewerb, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
NennungStatusBadge(nennung.status)
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NennungStatusBadge(status: String) {
|
||||
val (bg, fg) = when (status) {
|
||||
"Gemeldet" -> Color(0xFFDCFCE7) to Color(0xFF16A34A)
|
||||
"Bezahlt" -> Color(0xFFDBEAFE) to NennBlue
|
||||
"Abgemeldet" -> Color(0xFFFEE2E2) to Color(0xFFDC2626)
|
||||
else -> Color(0xFFF3F4F6) to Color(0xFF6B7280)
|
||||
}
|
||||
Surface(shape = MaterialTheme.shapes.small, color = bg) {
|
||||
Text(
|
||||
text = status,
|
||||
fontSize = 10.sp,
|
||||
color = fg,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerkaufBuchungenPanel() {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Verkauf / Buchungen", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
|
||||
// Artikel-Buchungen
|
||||
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("Artikel-Buchungen", fontSize = 12.sp, fontWeight = FontWeight.Medium, color = Color(0xFF374151))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(NennHeaderBg).padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text("Artikel", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Menge", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(50.dp))
|
||||
Text("Preis", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
Text(
|
||||
"Keine Buchungen",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF9CA3AF),
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp)
|
||||
) {
|
||||
Text("+ Artikel buchen", fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbsuebersichtPanel() {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Bewerbsübersicht", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(NennHeaderBg).padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text("Bewerb", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Nennungen", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(80.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
listOf(
|
||||
"Bewerb 1 – Dressur Kl. A" to 0,
|
||||
"Bewerb 2 – Dressur Kl. L" to 0,
|
||||
"Bewerb 3 – Springen Kl. A" to 0,
|
||||
).forEach { (name, count) ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(name, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
Text("$count", fontSize = 12.sp, modifier = Modifier.width(80.dp), color = Color(0xFF6B7280))
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── UI-Modell ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private data class NennungUiModel(
|
||||
val startnr: Int,
|
||||
val pferd: String,
|
||||
val reiter: String,
|
||||
val bewerb: String,
|
||||
val status: String,
|
||||
)
|
||||
|
||||
private fun sampleNennungen(): List<NennungUiModel> = emptyList()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
|
||||
/**
|
||||
* Formular zum Anlegen eines neuen Turniers (Vision_03: /veranstaltung/{id}/turnier/neu).
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
private val DeleteRed = Color(0xFFDC2626)
|
||||
|
||||
/**
|
||||
* ORGANISATION-Tab im TurnierDetailScreen.
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_13 / figma-entwurf_14):
|
||||
* - Funktionäre & Offizielle (C-Satz): Turnierleiter, Turnierbeauftragter, Technischer Delegierter, Parcourschef
|
||||
* - Support-Team: Tierarzt, Schmied, Steward
|
||||
* - Richterkollegium: dynamische Liste (Name, Qualifikation, Funktion, Löschen)
|
||||
* - Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen)
|
||||
*/
|
||||
@Composable
|
||||
fun OrganisationTabContent() {
|
||||
var turnierleiter by remember { mutableStateOf("") }
|
||||
var turnierbeauftragter by remember { mutableStateOf("") }
|
||||
var technischerDelegierter by remember { mutableStateOf("") }
|
||||
var parcourschef by remember { mutableStateOf("") }
|
||||
var tierarzt by remember { mutableStateOf("") }
|
||||
var schmied by remember { mutableStateOf("") }
|
||||
var steward by remember { mutableStateOf("") }
|
||||
|
||||
var richter by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
RichterUiModel("Alexandra Schuster", "D-GP", "Hauptrichter"),
|
||||
RichterUiModel("Ulrike Knasmüller-Prinz", "D-M", "Beisitzer"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var plaetze by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
AustragungsplatzUiModel("Dressur", "20 x 60 m", "Hauptplatz"),
|
||||
AustragungsplatzUiModel("Dressur", "20 x 40 m", "Abreiteplatz 1"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// ── Funktionäre & Offizielle ─────────────────────────────────────────
|
||||
OrgSectionCard(title = "Funktionäre & Offizielle (C-Satz)") {
|
||||
OrgSubSection("Turnier-Organisation") {
|
||||
OrgSearchField("Turnierleiter:", turnierleiter) { turnierleiter = it }
|
||||
OrgSearchField("Turnierbeauftragter:", turnierbeauftragter) { turnierbeauftragter = it }
|
||||
OrgSearchField("Technischer Delegierter:", technischerDelegierter) { technischerDelegierter = it }
|
||||
OrgSearchField("Parcourschef:", parcourschef) { parcourschef = it }
|
||||
}
|
||||
OrgSubSection("Support-Team") {
|
||||
OrgSearchField("Tierarzt:", tierarzt) { tierarzt = it }
|
||||
OrgSearchField("Schmied:", schmied) { schmied = it }
|
||||
OrgSearchField("Steward:", steward) { steward = it }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Richterkollegium ─────────────────────────────────────────────────
|
||||
OrgSectionCard(
|
||||
title = "Richterkollegium",
|
||||
action = {
|
||||
TextButton(onClick = {
|
||||
richter = richter + RichterUiModel("", "", "")
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp), tint = AccentBlue)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Richter hinzufügen", color = AccentBlue, fontSize = 13.sp)
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Tabellen-Header
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
|
||||
Text("Name", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
|
||||
Text("Qualifikation", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
|
||||
Text("Funktion", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
|
||||
Text("Aktion", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(48.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
richter.forEachIndexed { idx, r ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = r.name,
|
||||
onValueChange = { v -> richter = richter.toMutableList().also { it[idx] = r.copy(name = v) } },
|
||||
modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
// Qualifikation-Dropdown
|
||||
var qualExpanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = r.qualifikation,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
DropdownMenu(expanded = qualExpanded, onDismissRequest = { qualExpanded = false }) {
|
||||
listOf("D-GP", "D-M", "D-L", "S-GP", "S-M").forEach { q ->
|
||||
DropdownMenuItem(text = { Text(q) }, onClick = {
|
||||
richter = richter.toMutableList().also { it[idx] = r.copy(qualifikation = q) }
|
||||
qualExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Funktion-Dropdown
|
||||
var funExpanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = r.funktion,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
DropdownMenu(expanded = funExpanded, onDismissRequest = { funExpanded = false }) {
|
||||
listOf("Hauptrichter", "Beisitzer", "Schreiber").forEach { f ->
|
||||
DropdownMenuItem(text = { Text(f) }, onClick = {
|
||||
richter = richter.toMutableList().also { it[idx] = r.copy(funktion = f) }
|
||||
funExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = { richter = richter.toMutableList().also { it.removeAt(idx) } },
|
||||
modifier = Modifier.size(44.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Löschen",
|
||||
tint = DeleteRed,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Austragungsplätze ────────────────────────────────────────────────
|
||||
OrgSectionCard(
|
||||
title = "Austragungsplätze",
|
||||
) {
|
||||
OrgSubSection(
|
||||
title = "Plätze & Anlagen",
|
||||
action = {
|
||||
TextButton(onClick = {
|
||||
plaetze = plaetze + AustragungsplatzUiModel("", "", "")
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp), tint = AccentBlue)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Platz hinzufügen", color = AccentBlue, fontSize = 13.sp)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
|
||||
Text("Sparte", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
|
||||
Text("Größe", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
|
||||
Text("Bezeichnung", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
|
||||
Text("Aktion", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(48.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
plaetze.forEachIndexed { idx, p ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var sparteExpanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = p.sparte,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
DropdownMenu(expanded = sparteExpanded, onDismissRequest = { sparteExpanded = false }) {
|
||||
listOf("Dressur", "Springen", "Vielseitigkeit").forEach { s ->
|
||||
DropdownMenuItem(text = { Text(s) }, onClick = {
|
||||
plaetze = plaetze.toMutableList().also { it[idx] = p.copy(sparte = s) }
|
||||
sparteExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
var groesseExpanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = p.groesse,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
DropdownMenu(expanded = groesseExpanded, onDismissRequest = { groesseExpanded = false }) {
|
||||
listOf("20 x 60 m", "20 x 40 m", "60 x 80 m").forEach { g ->
|
||||
DropdownMenuItem(text = { Text(g) }, onClick = {
|
||||
plaetze = plaetze.toMutableList().also { it[idx] = p.copy(groesse = g) }
|
||||
groesseExpanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = p.bezeichnung,
|
||||
onValueChange = { v -> plaetze = plaetze.toMutableList().also { it[idx] = p.copy(bezeichnung = v) } },
|
||||
modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
IconButton(
|
||||
onClick = { plaetze = plaetze.toMutableList().also { it.removeAt(idx) } },
|
||||
modifier = Modifier.size(44.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Löschen",
|
||||
tint = DeleteRed,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Speichern ────────────────────────────────────────────────────────
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) { Text("Speichern") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrgSectionCard(
|
||||
title: String,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
||||
action?.invoke()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrgSubSection(
|
||||
title: String,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9FAFB)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(title, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
action?.invoke()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrgSearchField(label: String, value: String, onValueChange: (String) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(label, fontSize = 13.sp, modifier = Modifier.width(200.dp), color = Color(0xFF374151))
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
placeholder = { Text("Name suchen...", fontSize = 12.sp) },
|
||||
modifier = Modifier.weight(1f).height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI-Modelle ---
|
||||
|
||||
data class RichterUiModel(val name: String, val qualifikation: String, val funktion: String)
|
||||
data class AustragungsplatzUiModel(val sparte: String, val groesse: String, val bezeichnung: String)
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.filled.Usb
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
|
||||
/**
|
||||
* STAMMDATEN-Tab im TurnierDetailScreen.
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_16 / figma-entwurf_15):
|
||||
* - Turnier-Konfiguration: Nr., Typ (OTO/FEI), ZNS-Import, Sprache
|
||||
* - Sparten-Checkboxen, Klassen, Kategorien, Datum
|
||||
* - Turnier-Beschreibung: Titel, Sub-Titel
|
||||
* - Sponsoren
|
||||
*/
|
||||
@Composable
|
||||
fun StammdatenTabContent(turnierId: Long) {
|
||||
var turnierNr by remember { mutableStateOf("") }
|
||||
var typOto by remember { mutableStateOf(true) }
|
||||
var spracheDe by remember { mutableStateOf(true) }
|
||||
var sparteDressur by remember { mutableStateOf(false) }
|
||||
var sparteSpringen by remember { mutableStateOf(false) }
|
||||
var klasseC by remember { mutableStateOf(false) }
|
||||
var klasseB by remember { mutableStateOf(false) }
|
||||
var klasseA by remember { mutableStateOf(false) }
|
||||
var datumVon by remember { mutableStateOf("") }
|
||||
var datumBis by remember { mutableStateOf("") }
|
||||
var titel by remember { mutableStateOf("") }
|
||||
var subTitel by remember { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// ── Turnier-Konfiguration ────────────────────────────────────────────
|
||||
SectionCard(title = "Turnier-Konfiguration") {
|
||||
FormRow("Turnier-Nr.:") {
|
||||
OutlinedTextField(
|
||||
value = turnierNr,
|
||||
onValueChange = { turnierNr = it },
|
||||
placeholder = { Text("z.B. 26128", fontSize = 13.sp) },
|
||||
modifier = Modifier.width(200.dp).height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
FormRow("Typ:") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = typOto, onClick = { typOto = true })
|
||||
Text("OTO (National)", fontSize = 13.sp)
|
||||
RadioButton(selected = !typOto, onClick = { typOto = false })
|
||||
Text("FEI (International)", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
FormRow("ZNS-Daten:") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue),
|
||||
) {
|
||||
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Import via Internet", fontSize = 13.sp)
|
||||
}
|
||||
OutlinedButton(onClick = {}) {
|
||||
Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Import via USB", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
"Reiter-, Pferde-, Funktionärs- und Vereinsdaten vom OEPS Backend",
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
}
|
||||
FormRow("Sprache:") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = spracheDe, onClick = { spracheDe = true })
|
||||
Text("Deutsch", fontSize = 13.sp)
|
||||
RadioButton(selected = !spracheDe, onClick = { spracheDe = false })
|
||||
Text("English", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
FormRow("Sparten:") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = sparteDressur, onCheckedChange = { sparteDressur = it })
|
||||
Text("Dressur", fontSize = 13.sp)
|
||||
Checkbox(checked = sparteSpringen, onCheckedChange = { sparteSpringen = it })
|
||||
Text("Springen", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
FormRow("Klassen:") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = klasseC, onCheckedChange = { klasseC = it })
|
||||
Text("C", fontSize = 13.sp)
|
||||
Checkbox(checked = klasseB, onCheckedChange = { klasseB = it })
|
||||
Text("B", fontSize = 13.sp)
|
||||
Checkbox(checked = klasseA, onCheckedChange = { klasseA = it })
|
||||
Text("A", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
OutlinedTextField(
|
||||
value = datumVon,
|
||||
onValueChange = { datumVon = it },
|
||||
placeholder = { Text("DD.MM.YYYY", fontSize = 12.sp) },
|
||||
modifier = Modifier.width(160.dp).height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
Text("bis", fontSize = 13.sp)
|
||||
OutlinedTextField(
|
||||
value = datumBis,
|
||||
onValueChange = { datumBis = it },
|
||||
placeholder = { Text("DD.MM.YYYY", fontSize = 12.sp) },
|
||||
modifier = Modifier.width(160.dp).height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Turnier-Beschreibung ─────────────────────────────────────────────
|
||||
SectionCard(title = "Turnier-Beschreibung") {
|
||||
OutlinedTextField(
|
||||
value = titel,
|
||||
onValueChange = { titel = it },
|
||||
placeholder = { Text("z.B. Frühjahrs-Turnier 2026", fontSize = 13.sp) },
|
||||
label = { Text("Titel") },
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = subTitel,
|
||||
onValueChange = { subTitel = it },
|
||||
placeholder = { Text("z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ", fontSize = 13.sp) },
|
||||
label = { Text("Sub-Titel") },
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Sponsoren ────────────────────────────────────────────────────────
|
||||
SectionCard(
|
||||
title = "Sponsoren",
|
||||
action = {
|
||||
TextButton(onClick = {}) {
|
||||
Text("+ Sponsor hinzufügen", color = AccentBlue, fontSize = 13.sp)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().height(80.dp),
|
||||
color = Color(0xFFF9FAFB),
|
||||
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 ──────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(onClick = {}) { Text("Zurücksetzen") }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) { Text("Speichern") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionCard(
|
||||
title: String,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
||||
action?.invoke()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FormRow(label: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.width(140.dp).padding(top = 12.dp),
|
||||
color = Color(0xFF374151),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val SlBlue = Color(0xFF1E3A8A)
|
||||
private val SlHeaderBg = Color(0xFFF1F5F9)
|
||||
|
||||
/**
|
||||
* STARTLISTEN-Tab gemäß Vision_03.
|
||||
*
|
||||
* Layout: 2-spaltig
|
||||
* - Links (flex): Bewerbs-Tabs + Starter-Tabelle (Startnr | Pferd | Reiter | Abteilung | Beginn)
|
||||
* - Rechts (280dp): Sortierung & Zeit-Panel
|
||||
*/
|
||||
@Composable
|
||||
fun StartlistenTabContent() {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Linke Spalte: Bewerbs-Tabs + Tabelle ─────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
StartlistenBewerbsTabs()
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
// ── Rechte Spalte: Sortierung & Zeit ─────────────────────────────────
|
||||
StartlistenSortierPanel(modifier = Modifier.width(280.dp).fillMaxHeight())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartlistenBewerbsTabs() {
|
||||
val bewerbe = remember {
|
||||
listOf("Bewerb 1", "Bewerb 2", "Bewerb 3", "Bewerb 4", "Bewerb 5")
|
||||
}
|
||||
var selectedBewerb by remember { mutableIntStateOf(0) }
|
||||
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedBewerb,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = SlBlue,
|
||||
edgePadding = 0.dp,
|
||||
) {
|
||||
bewerbe.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedBewerb == index,
|
||||
onClick = { selectedBewerb = index },
|
||||
text = { Text(title, fontSize = 12.sp) },
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Bewerb ${selectedBewerb + 1} – Startliste",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp)
|
||||
) {
|
||||
Text("Drucken", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp)
|
||||
) {
|
||||
Text("Exportieren", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Tabellen-Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(SlHeaderBg).padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text("Startnr.", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
|
||||
Text("Pferd", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Reiter", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
Text("Abteilung", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(80.dp))
|
||||
Text("Beginn", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
// Leere Liste
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Keine Starter vorhanden", fontSize = 14.sp, color = Color(0xFF6B7280))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Startliste wird nach Nennungsschluss generiert.", fontSize = 12.sp, color = Color(0xFF9CA3AF))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = SlBlue),
|
||||
) {
|
||||
Text("Startliste generieren", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartlistenSortierPanel(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Sortierung & Zeit", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
HorizontalDivider()
|
||||
|
||||
// Sortierung
|
||||
Text("Sortierung", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
SortierOption("Aufsteigend (Startnummer)")
|
||||
SortierOption("Absteigend (Startnummer)")
|
||||
SortierOption("Auslosung (zufällig)")
|
||||
SortierOption("Alphabetisch (Pferd)")
|
||||
SortierOption("Alphabetisch (Reiter)")
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Zeiten
|
||||
Text("Zeiten", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
|
||||
LabeledInput("Beginnzeit:", "08:00", "(hh:mm)")
|
||||
LabeledInput("Reitdauer:", "02:00", "(mm:ss)")
|
||||
LabeledInput("Umbau:", "10", "(mm)")
|
||||
LabeledInput("Besichtigung:", "10", "(mm)")
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Button(
|
||||
onClick = {},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = SlBlue),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Zeiten neu berechnen", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SortierOption(label: String) {
|
||||
var selected by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
) {
|
||||
RadioButton(selected = selected, onClick = { selected = !selected })
|
||||
Text(label, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabeledInput(label: String, value: String, unit: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label, fontSize = 12.sp, modifier = Modifier.width(100.dp))
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.width(70.dp),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(unit, fontSize = 11.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
31
frontend/features/veranstalter-feature/build.gradle.kts
Normal file
31
frontend/features/veranstalter-feature/build.gradle.kts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Feature-Modul: Veranstalter-Verwaltung (Desktop-only)
|
||||
* Kapselt alle Screens und Logik für Veranstalter-Auswahl, -Detail und -Neuanlage.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatus
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatusBadge
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
|
||||
/**
|
||||
* Screen: "Admin - Verwaltung / Veranstalter auswählen"
|
||||
*
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_20 / figma-entwurf_22):
|
||||
* - Titel + Untertitel
|
||||
* - Suchfeld + "+ Neuer Veranstalter"-Button
|
||||
* - Tabelle: Vereinsname, OEPS-Nummer, Ort, Ansprechpartner, E-Mail, Login-Status
|
||||
* - Hinweis-Box
|
||||
* - Abbrechen / "Weiter zum Veranstalter"-Buttons (unten)
|
||||
*
|
||||
* TODO: Echte Daten aus customer-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstalterAuswahlScreen(
|
||||
onZurueck: () -> Unit,
|
||||
onWeiter: (Long) -> Unit,
|
||||
onNeuerVeranstalter: () -> Unit = {},
|
||||
) {
|
||||
var selectedId by remember { mutableStateOf<Long?>(null) }
|
||||
var suchtext by remember { mutableStateOf("") }
|
||||
|
||||
// Placeholder-Daten gemäß Figma
|
||||
val veranstalter = remember {
|
||||
listOf(
|
||||
VeranstalterUiModel(
|
||||
id = 1L,
|
||||
name = "Reit- und Fahrverein Wels",
|
||||
oepsNummer = "V-OOE-1234",
|
||||
ort = "4600 Wels",
|
||||
ansprechpartner = "Maria Huber",
|
||||
email = "office@rfv-wels.at",
|
||||
loginStatus = LoginStatus.AKTIV,
|
||||
),
|
||||
VeranstalterUiModel(
|
||||
id = 2L,
|
||||
name = "Pferdesportverein Linz",
|
||||
oepsNummer = "V-OOE-5678",
|
||||
ort = "4020 Linz",
|
||||
ansprechpartner = "Thomas Maier",
|
||||
email = "kontakt@psv-linz.at",
|
||||
loginStatus = LoginStatus.AKTIV,
|
||||
),
|
||||
VeranstalterUiModel(
|
||||
id = 3L,
|
||||
name = "Reitclub Eferding",
|
||||
oepsNummer = "V-OOE-9012",
|
||||
ort = "4070 Eferding",
|
||||
ansprechpartner = "Anna Schmid",
|
||||
email = "info@rc-eferding.at",
|
||||
loginStatus = LoginStatus.AUSSTEHEND,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val gefiltert = veranstalter.filter {
|
||||
suchtext.isBlank() ||
|
||||
it.name.contains(suchtext, ignoreCase = true) ||
|
||||
it.oepsNummer.contains(suchtext, ignoreCase = true) ||
|
||||
it.ort.contains(suchtext, ignoreCase = true)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// ── Titel ────────────────────────────────────────────────────────────
|
||||
Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) {
|
||||
Text(
|
||||
text = "Veranstalter für neue Veranstaltung auswählen",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = "Wählen Sie einen bestehenden Veranstalter aus oder legen Sie einen neuen Veranstalter an.",
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Suchfeld + Neuer Veranstalter ────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = suchtext,
|
||||
onValueChange = { suchtext = it },
|
||||
placeholder = { Text("Veranstalter suchen (Name, OEPS-Nummer, Ort)...", fontSize = 13.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier.weight(1f).height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
Button(
|
||||
onClick = onNeuerVeranstalter,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neuer Veranstalter")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tabellen-Header ──────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFFF3F4F6))
|
||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
) {
|
||||
Spacer(Modifier.width(28.dp)) // Checkmark-Spalte
|
||||
Text("Vereinsname", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(2.5f))
|
||||
Text("OEPS-Nummer", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
|
||||
Text("Ort", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
|
||||
Text("Ansprechpartner", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
|
||||
Text("E-Mail", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(2f))
|
||||
Text("Login", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
// ── Tabellen-Inhalt ──────────────────────────────────────────────────
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
items(gefiltert) { v ->
|
||||
val isSelected = v.id == selectedId
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
if (isSelected) AccentBlue.copy(alpha = 0.08f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable { selectedId = v.id }
|
||||
.padding(horizontal = 24.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Auswahl-Checkmark
|
||||
Box(modifier = Modifier.width(28.dp)) {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = AccentBlue,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = v.name,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
color = AccentBlue,
|
||||
modifier = Modifier.weight(2.5f),
|
||||
)
|
||||
Text(v.oepsNummer, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
|
||||
Text(v.ort, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
|
||||
Text(v.ansprechpartner, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
|
||||
Text(v.email, fontSize = 13.sp, modifier = Modifier.weight(2f))
|
||||
// Login-Status-Badge
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
LoginStatusBadge(v.loginStatus)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hinweis-Box ──────────────────────────────────────────────────────
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
color = Color(0xFFEFF6FF),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFBFDBFE)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
tint = AccentBlue,
|
||||
modifier = Modifier.size(16.dp).padding(top = 2.dp),
|
||||
)
|
||||
Text(
|
||||
text = "Veranstalter sind Vereine, die beim österreichischen Pferdesportverband (OEPS) registriert sind. " +
|
||||
"Beim Anlegen eines neuen Veranstalters werden automatisch Login-Daten generiert und per E-Mail verschickt. " +
|
||||
"Der Veranstalter kann dann sein Profil (Logo, Kontaktdaten, etc.) selbst verwalten.",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF1E40AF),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ── Aktions-Buttons ──────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(onClick = onZurueck) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Button(
|
||||
onClick = { selectedId?.let { onWeiter(it) } },
|
||||
enabled = selectedId != null,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Text("Weiter zum Veranstalter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- UI-Modelle ---
|
||||
|
||||
data class VeranstalterUiModel(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val oepsNummer: String,
|
||||
val ort: String,
|
||||
val ansprechpartner: String,
|
||||
val email: String,
|
||||
val loginStatus: LoginStatus,
|
||||
)
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatus
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatusBadge
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
private val StatusVorbereitungColor = Color(0xFFEA580C)
|
||||
private val StatusLiveColor = Color(0xFF16A34A)
|
||||
private val StatusAbgeschlossenColor = Color(0xFF6B7280)
|
||||
|
||||
/**
|
||||
* Screen: "Admin - Verwaltung / Veranstalter auswählen / <Vereinsname>"
|
||||
*
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_18 / figma-entwurf_19):
|
||||
* - Veranstalter-Header: Avatar-Circle, Name, OEPS-Nr., Kontaktdetails-Grid, Login-Status, Mitglied-seit
|
||||
* - Aktionsleiste: "+ Neue Veranstaltung", Suchfeld, Status-Filter-Chips
|
||||
* - Veranstaltungs-Liste: Status-Badge, Datum, Ort, Turniere, Nennungen, Bewerbe, Letzte Aktivität, Bearbeiten-Icon
|
||||
*
|
||||
* TODO: Echte Daten aus customer-context / event-management-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstalterDetailScreen(
|
||||
veranstalterId: Long,
|
||||
onZurueck: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onVeranstaltungNeu: () -> Unit,
|
||||
) {
|
||||
var suchtext by remember { mutableStateOf("") }
|
||||
var statusFilter by remember { mutableStateOf(VeranstaltungStatusFilter.ALLE) }
|
||||
|
||||
// Placeholder-Daten gemäß Figma
|
||||
val veranstalter = remember(veranstalterId) {
|
||||
VeranstalterDetailUiModel(
|
||||
id = veranstalterId,
|
||||
name = "Reit- und Fahrverein Wels",
|
||||
oepsNummer = "V-OOE-1234",
|
||||
ansprechpartner = "Maria Huber",
|
||||
email = "office@rfv-wels.at",
|
||||
telefon = "+43 7242 12345",
|
||||
adresse = "Reitweg 15\n4600 Wels",
|
||||
loginStatus = LoginStatus.AKTIV,
|
||||
mitgliedSeit = "15.1.2023",
|
||||
)
|
||||
}
|
||||
|
||||
val veranstaltungen = remember(veranstalterId) {
|
||||
listOf(
|
||||
VeranstaltungListUiModel(
|
||||
id = 1L,
|
||||
name = "Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026",
|
||||
datum = "25.-26. April 2026",
|
||||
ort = "Reitanlage Stroblmair, Neumarkt/M., OO",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 87,
|
||||
bewerbe = 26,
|
||||
letzteAktivitaet = "22.03.2026 14:30",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 2L,
|
||||
name = "AWÖ-Cup Stadl-Paura 2025",
|
||||
datum = "15.-17. Mai 2025",
|
||||
ort = "Bundesgestüt Piber, Stadl-Paura",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 142,
|
||||
bewerbe = 33,
|
||||
letzteAktivitaet = "17.05.2025 18:45",
|
||||
status = VeranstaltungStatus.ABGESCHLOSSEN,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 3L,
|
||||
name = "Linzer Pferdetage 2026",
|
||||
datum = "12.-14. Juni 2026",
|
||||
ort = "Reitsportzentrum Linz-Ebelsberg",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 23,
|
||||
bewerbe = 30,
|
||||
letzteAktivitaet = "20.03.2026 09:15",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val gefiltert = veranstaltungen.filter { v ->
|
||||
val matchesStatus = when (statusFilter) {
|
||||
VeranstaltungStatusFilter.ALLE -> true
|
||||
VeranstaltungStatusFilter.VORBEREITUNG -> v.status == VeranstaltungStatus.VORBEREITUNG
|
||||
VeranstaltungStatusFilter.LIVE -> v.status == VeranstaltungStatus.LIVE
|
||||
VeranstaltungStatusFilter.ABGESCHLOSSEN -> v.status == VeranstaltungStatus.ABGESCHLOSSEN
|
||||
}
|
||||
val matchesSuche = suchtext.isBlank() ||
|
||||
v.name.contains(suchtext, ignoreCase = true) ||
|
||||
v.ort.contains(suchtext, ignoreCase = true)
|
||||
matchesStatus && matchesSuche
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// ── Veranstalter-Header-Card ─────────────────────────────────────────
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
// Avatar-Circle
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = veranstalter.name.first().uppercase(),
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = veranstalter.name,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = "OEPS-Nummer: ${veranstalter.oepsNummer}",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
// Kontaktdetails-Grid
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
KontaktSpalte("Ansprechpartner", veranstalter.ansprechpartner)
|
||||
KontaktSpalte("E-Mail", veranstalter.email)
|
||||
KontaktSpalte("Telefon", veranstalter.telefon)
|
||||
KontaktSpalte("Adresse", veranstalter.adresse)
|
||||
Column {
|
||||
Text("Login-Status", fontSize = 11.sp, color = Color(0xFF9CA3AF))
|
||||
Spacer(Modifier.height(2.dp))
|
||||
LoginStatusBadge(veranstalter.loginStatus)
|
||||
}
|
||||
KontaktSpalte("Mitglied seit", veranstalter.mitgliedSeit)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Profil bearbeiten
|
||||
OutlinedButton(
|
||||
onClick = { /* TODO */ },
|
||||
border = BorderStroke(1.dp, Color(0xFFD1D5DB)),
|
||||
) {
|
||||
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Profil bearbeiten", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aktionsleiste ────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = onVeranstaltungNeu,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = suchtext,
|
||||
onValueChange = { suchtext = it },
|
||||
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 12.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
||||
modifier = Modifier.weight(1f).height(44.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
// Status-Filter-Chips
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
VeranstaltungStatusFilter.entries.forEach { filter ->
|
||||
val isActive = statusFilter == filter
|
||||
FilterChip(
|
||||
selected = isActive,
|
||||
onClick = { statusFilter = filter },
|
||||
label = {
|
||||
Text(
|
||||
text = when (filter) {
|
||||
VeranstaltungStatusFilter.ALLE -> "Alle"
|
||||
VeranstaltungStatusFilter.VORBEREITUNG -> "Vorbereitung"
|
||||
VeranstaltungStatusFilter.LIVE -> "Live"
|
||||
VeranstaltungStatusFilter.ABGESCHLOSSEN -> "Abgeschlossen"
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = PrimaryBlue,
|
||||
selectedLabelColor = Color.White,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Veranstaltungs-Liste ─────────────────────────────────────────────
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
) {
|
||||
items(gefiltert) { veranstaltung ->
|
||||
VeranstaltungListRow(
|
||||
veranstaltung = veranstaltung,
|
||||
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KontaktSpalte(label: String, wert: String) {
|
||||
Column {
|
||||
Text(label, fontSize = 11.sp, color = Color(0xFF9CA3AF))
|
||||
Text(wert, fontSize = 12.sp, color = Color(0xFF374151))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VeranstaltungListRow(
|
||||
veranstaltung: VeranstaltungListUiModel,
|
||||
onOeffnen: () -> Unit,
|
||||
) {
|
||||
val statusColor = when (veranstaltung.status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> StatusVorbereitungColor
|
||||
VeranstaltungStatus.LIVE -> StatusLiveColor
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> StatusAbgeschlossenColor
|
||||
}
|
||||
val statusText = when (veranstaltung.status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung"
|
||||
VeranstaltungStatus.LIVE -> "Live"
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen"
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
border = BorderStroke(1.dp, Color(0xFFE5E7EB)),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Status-Badge
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
color = statusColor.copy(alpha = 0.15f),
|
||||
modifier = Modifier.width(100.dp),
|
||||
) {
|
||||
Text(
|
||||
text = statusText,
|
||||
color = statusColor,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
// Name + Meta
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = veranstaltung.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
// Statistiken
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
StatSpalte("Nennungen", "${veranstaltung.nennungen}")
|
||||
StatSpalte("Bewerbe", "${veranstaltung.bewerbe}")
|
||||
StatSpalte("Letzte Aktivität", veranstaltung.letzteAktivitaet)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
// Bearbeiten-Icon
|
||||
IconButton(onClick = onOeffnen) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "Öffnen",
|
||||
tint = Color(0xFF9CA3AF),
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatSpalte(label: String, wert: String) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(label, fontSize = 10.sp, color = Color(0xFF9CA3AF))
|
||||
Text(wert, fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI-Modelle ---
|
||||
|
||||
enum class VeranstaltungStatusFilter { ALLE, VORBEREITUNG, LIVE, ABGESCHLOSSEN }
|
||||
|
||||
data class VeranstalterDetailUiModel(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val oepsNummer: String,
|
||||
val ansprechpartner: String,
|
||||
val email: String,
|
||||
val telefon: String,
|
||||
val adresse: String,
|
||||
val loginStatus: LoginStatus,
|
||||
val mitgliedSeit: String,
|
||||
)
|
||||
|
||||
data class VeranstaltungListUiModel(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val datum: String,
|
||||
val ort: String,
|
||||
val turnierAnzahl: Int,
|
||||
val nennungen: Int,
|
||||
val bewerbe: Int,
|
||||
val letzteAktivitaet: String,
|
||||
val status: VeranstaltungStatus,
|
||||
)
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
package at.mocode.veranstalter.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Formular zum Anlegen eines neuen Veranstalters (Vision_03: Screenshot 21).
|
||||
*
|
||||
* Layout:
|
||||
* - Info-Banner: "Login-Daten werden automatisch verschickt"
|
||||
* - Abschnitt "Vereinsdaten": Vereinsname*, OEPS-Nummer*
|
||||
* - Abschnitt "Kontaktdaten": Ansprechpartner*, E-Mail*, Telefon
|
||||
* - Abschnitt "Adresse": Straße & Hausnummer, PLZ + Ort
|
||||
* - Footer-Buttons: Abbrechen | Veranstalter anlegen & Login-Daten senden
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstalterNeuScreen(
|
||||
onAbbrechen: () -> Unit,
|
||||
onSpeichern: (vereinsname: String, oepsNummer: String, email: String) -> Unit,
|
||||
) {
|
||||
var vereinsname by remember { mutableStateOf("") }
|
||||
var oepsNummer by remember { mutableStateOf("") }
|
||||
var ansprechpartner by remember { mutableStateOf("") }
|
||||
var email by remember { mutableStateOf("") }
|
||||
var telefon by remember { mutableStateOf("") }
|
||||
var strasse by remember { mutableStateOf("") }
|
||||
var plz by remember { mutableStateOf("") }
|
||||
var ort by remember { mutableStateOf("") }
|
||||
|
||||
val isValid = vereinsname.isNotBlank() && oepsNummer.isNotBlank() &&
|
||||
ansprechpartner.isNotBlank() && email.isNotBlank()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
// Header
|
||||
Column(modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp)) {
|
||||
Text(
|
||||
text = "Neuen Veranstalter anlegen",
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Legen Sie einen neuen Veranstalter (Verein) mit OEPS-Daten an. Nach dem Speichern werden automatisch Login-Daten generiert.",
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
}
|
||||
|
||||
// Info-Banner
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 40.dp),
|
||||
color = Color(0xFFEFF6FF),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF2563EB),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = "Login-Daten werden automatisch verschickt",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF1E40AF),
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = "Nach dem Anlegen werden Login-Daten generiert und an die angegebene E-Mail-Adresse verschickt. Der Veranstalter kann dann sein Profil selbst vervollständigen.",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF1E40AF),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Formular-Card
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 40.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
|
||||
// --- Vereinsdaten ---
|
||||
Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||
|
||||
OutlinedTextField(
|
||||
value = vereinsname,
|
||||
onValueChange = { vereinsname = it },
|
||||
label = { Text("Vereinsname *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = oepsNummer,
|
||||
onValueChange = { oepsNummer = it },
|
||||
label = { Text("OEPS-Nummer *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
Text(
|
||||
text = "Offizielle Vereinsnummer des OEPS",
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFF2563EB),
|
||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// --- Kontaktdaten ---
|
||||
Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||
|
||||
OutlinedTextField(
|
||||
value = ansprechpartner,
|
||||
onValueChange = { ansprechpartner = it },
|
||||
label = { Text("Ansprechpartner *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("E-Mail *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
Text(
|
||||
text = "Login-Daten werden an diese Adresse verschickt",
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = telefon,
|
||||
onValueChange = { telefon = it },
|
||||
label = { Text("Telefon") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// --- Adresse ---
|
||||
Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||
|
||||
OutlinedTextField(
|
||||
value = strasse,
|
||||
onValueChange = { strasse = it },
|
||||
label = { Text("Straße & Hausnummer") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = plz,
|
||||
onValueChange = { plz = it },
|
||||
label = { Text("PLZ") },
|
||||
modifier = Modifier.width(120.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = ort,
|
||||
onValueChange = { ort = it },
|
||||
label = { Text("Ort") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Footer-Buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 40.dp, vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(onClick = onAbbrechen) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Button(
|
||||
onClick = { onSpeichern(vereinsname, oepsNummer, email) },
|
||||
enabled = isValid,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
) {
|
||||
Text("Veranstalter anlegen & Login-Daten senden")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
31
frontend/features/veranstaltung-feature/build.gradle.kts
Normal file
31
frontend/features/veranstaltung-feature/build.gradle.kts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Feature-Modul: Veranstaltungs-Verwaltung (Desktop-only)
|
||||
* Kapselt alle Screens und Logik für Veranstaltungs-Übersicht, -Detail und -Neuanlage.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
|
||||
// Status-Farben gemäß Vision_03
|
||||
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
||||
|
|
@ -35,6 +36,7 @@ private val StatusAbgeschlossen = Color(0xFF6B7280) // Grau
|
|||
fun AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onPingService: () -> Unit = {},
|
||||
) {
|
||||
// Placeholder-Daten für die UI-Struktur
|
||||
val veranstaltungen = listOf<VeranstaltungUiModel>() // leer bis Backend angebunden
|
||||
|
|
@ -73,6 +75,10 @@ fun AdminUebersichtScreen(
|
|||
singleLine = true,
|
||||
)
|
||||
|
||||
OutlinedButton(onClick = onPingService) {
|
||||
Text("🔧 Ping")
|
||||
}
|
||||
|
||||
// Status-Filter Chips
|
||||
StatusFilterChip("Alle", selected = true)
|
||||
StatusFilterChip("Vorbereitung", selected = false)
|
||||
|
|
@ -345,4 +351,3 @@ data class TurnierUiModel(
|
|||
val bewerbAnzahl: Int,
|
||||
)
|
||||
|
||||
enum class VeranstaltungStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -9,6 +9,7 @@ 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.models.PlaceholderContent
|
||||
|
||||
/**
|
||||
* Detailansicht einer bestehenden Veranstaltung (Vision_03: /veranstaltung/{id}).
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
|
||||
/**
|
||||
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.Add
|
||||
import androidx.compose.material.icons.filled.FileDownload
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.Usb
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val ImportOrange = Color(0xFFEA580C)
|
||||
private val ExportBlue = Color(0xFF2563EB)
|
||||
private val UsbColor = Color(0xFF7C3AED)
|
||||
|
||||
/**
|
||||
* Screen: "Veranstaltung - Übersicht"
|
||||
*
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_17):
|
||||
* - Veranstaltungs-Header (Name, Datum, Ort, Status)
|
||||
* - Liste aller Turniere dieser Veranstaltung als Cards
|
||||
* - Jede Turnier-Card hat Buttons:
|
||||
* - "Öffnen" → Meldestelle des Turniers öffnen (TurnierDetail)
|
||||
* - "Import" → Datenbank-Sicherung importieren
|
||||
* - "Export" → Datenbank-Sicherung exportieren
|
||||
* - "ZNS" → ZNS-Import für dieses Turnier starten
|
||||
*
|
||||
* Warum ZNS hier? Jedes Turnier hat seine eigene Datenbank/Kassa.
|
||||
* Der ZNS-Import muss daher turnierspezifisch sein.
|
||||
*
|
||||
* TODO: Echte Daten aus event-management-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstaltungUebersichtScreen(
|
||||
veranstalterId: Long,
|
||||
veranstaltungId: Long,
|
||||
onZurueck: () -> Unit,
|
||||
onTurnierOeffnen: (turnierId: Long) -> Unit,
|
||||
onTurnierNeu: () -> Unit,
|
||||
onZnsImport: (turnierId: Long) -> Unit,
|
||||
onDbImport: (turnierId: Long) -> Unit,
|
||||
onDbExport: (turnierId: Long) -> Unit,
|
||||
) {
|
||||
// Placeholder-Daten
|
||||
val veranstaltung = remember(veranstaltungId) {
|
||||
VeranstaltungUiModel(
|
||||
id = veranstaltungId,
|
||||
name = "Frühjahrsturnier Wels 2026",
|
||||
ort = "Wels",
|
||||
datum = "15.04.2026",
|
||||
turnierAnzahl = 3,
|
||||
nennungen = 47,
|
||||
letzteAktivitaet = "heute",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
)
|
||||
}
|
||||
|
||||
val turniere = remember(veranstaltungId) {
|
||||
listOf(
|
||||
TurnierKarteUiModel(
|
||||
id = 1L,
|
||||
nummer = 26128L,
|
||||
name = "Dressurturnier",
|
||||
sparte = "Dressur",
|
||||
bewerbAnzahl = 8,
|
||||
nennungen = 24,
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
datum = "15.04.2026",
|
||||
),
|
||||
TurnierKarteUiModel(
|
||||
id = 2L,
|
||||
nummer = 26129L,
|
||||
name = "Springturnier",
|
||||
sparte = "Springen",
|
||||
bewerbAnzahl = 6,
|
||||
nennungen = 18,
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
datum = "15.04.2026",
|
||||
),
|
||||
TurnierKarteUiModel(
|
||||
id = 3L,
|
||||
nummer = 26130L,
|
||||
name = "Vielseitigkeitsturnier",
|
||||
sparte = "Vielseitigkeit",
|
||||
bewerbAnzahl = 4,
|
||||
nennungen = 5,
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
datum = "16.04.2026",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Tab-Header gemäß Figma
|
||||
TabRow(
|
||||
selectedTabIndex = 0,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Tab(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
text = { Text("VERANSTALTUNG - ÜBERSICHT", fontSize = 13.sp, fontWeight = FontWeight.Bold) },
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
// Veranstaltungs-Header
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color(0xFFF8FAFC),
|
||||
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = veranstaltung.name,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("📍 ${veranstaltung.ort}", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("📅 ${veranstaltung.datum}", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("📋 ${veranstaltung.nennungen} Nennungen gesamt", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = onZurueck) {
|
||||
Text("← Zurück")
|
||||
}
|
||||
Button(
|
||||
onClick = onTurnierNeu,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neues Turnier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Turnier-Liste
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Turniere (${turniere.size})",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Jedes Turnier hat eine eigene Datenbank und Kassa.",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
) {
|
||||
items(turniere) { turnier ->
|
||||
TurnierKarte(
|
||||
turnier = turnier,
|
||||
onOeffnen = { onTurnierOeffnen(turnier.id) },
|
||||
onZns = { onZnsImport(turnier.id) },
|
||||
onImport = { onDbImport(turnier.id) },
|
||||
onExport = { onDbExport(turnier.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TurnierKarte(
|
||||
turnier: TurnierKarteUiModel,
|
||||
onOeffnen: () -> Unit,
|
||||
onZns: () -> Unit,
|
||||
onImport: () -> Unit,
|
||||
onExport: () -> Unit,
|
||||
) {
|
||||
val statusColor = when (turnier.status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> Color(0xFFEA580C)
|
||||
VeranstaltungStatus.LIVE -> Color(0xFF16A34A)
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> Color(0xFF6B7280)
|
||||
}
|
||||
val statusText = when (turnier.status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung"
|
||||
VeranstaltungStatus.LIVE -> "Live"
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen"
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Turnier-Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
// Turnier-Nummer Badge
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = PrimaryBlue,
|
||||
) {
|
||||
Text(
|
||||
text = "${turnier.nummer}",
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = turnier.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🏇 ${turnier.sparte}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("📅 ${turnier.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("${turnier.bewerbAnzahl} Bewerbe", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("${turnier.nennungen} Nennungen", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Status-Badge
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = statusColor.copy(alpha = 0.15f),
|
||||
border = BorderStroke(1.dp, statusColor),
|
||||
) {
|
||||
Text(
|
||||
text = statusText,
|
||||
color = statusColor,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// Aktions-Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Öffnen – Hauptaktion
|
||||
Button(
|
||||
onClick = onOeffnen,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FolderOpen,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Öffnen", fontSize = 13.sp)
|
||||
}
|
||||
|
||||
// USB-Import
|
||||
OutlinedButton(
|
||||
onClick = onZns,
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Usb,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("USB", fontSize = 13.sp)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
// Import / Export – DB-Sicherung
|
||||
OutlinedButton(
|
||||
onClick = onImport,
|
||||
border = BorderStroke(1.dp, ImportOrange),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FileUpload,
|
||||
contentDescription = null,
|
||||
tint = ImportOrange,
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Import", fontSize = 13.sp, color = ImportOrange)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onExport,
|
||||
border = BorderStroke(1.dp, ExportBlue),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FileDownload,
|
||||
contentDescription = null,
|
||||
tint = ExportBlue,
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Export", fontSize = 13.sp, color = ExportBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI-Modelle ---
|
||||
|
||||
data class TurnierKarteUiModel(
|
||||
val id: Long,
|
||||
val nummer: Long,
|
||||
val name: String,
|
||||
val sparte: String,
|
||||
val bewerbAnzahl: Int,
|
||||
val nennungen: Int,
|
||||
val status: VeranstaltungStatus,
|
||||
val datum: String,
|
||||
)
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -11,6 +11,7 @@ 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.models.PlaceholderContent
|
||||
|
||||
/**
|
||||
* Veranstaltungs-Übersicht (Drawer-Einstieg gemäß Vision_03).
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
/**
|
||||
* Feature-Modul: ZNS-Stammdaten-Import (Desktop-only)
|
||||
* Kapselt ViewModel, State und API-Kommunikation für den ZNS-Import.
|
||||
* Kapselt ViewModel, State, API-Kommunikation und UI-Screen für den ZNS-Import.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
group = "at.mocode.clients"
|
||||
|
|
@ -13,8 +14,17 @@ kotlin {
|
|||
jvm()
|
||||
sourceSets {
|
||||
jvmMain.dependencies {
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.auth)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.ktor.client.core)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.zns.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -10,6 +10,7 @@ plugins {
|
|||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
id("org.jetbrains.compose.hot-reload")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
|
@ -30,6 +31,9 @@ kotlin {
|
|||
implementation(projects.frontend.features.nennungFeature)
|
||||
implementation(projects.frontend.features.pingFeature)
|
||||
implementation(projects.frontend.features.znsImportFeature)
|
||||
implementation(projects.frontend.features.veranstalterFeature)
|
||||
implementation(projects.frontend.features.veranstaltungFeature)
|
||||
implementation(projects.frontend.features.turnierFeature)
|
||||
|
||||
// Compose Desktop
|
||||
implementation(compose.desktop.currentOs)
|
||||
|
|
@ -39,6 +43,7 @@ kotlin {
|
|||
implementation(compose.ui)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.uiTooling)
|
||||
implementation(libs.composeHotReloadApi)
|
||||
|
||||
// DI (Koin)
|
||||
implementation(libs.koin.core)
|
||||
|
|
@ -59,6 +64,7 @@ kotlin {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "at.mocode.desktop.MainKt"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import at.mocode.desktop.navigation.DesktopNavigationPort
|
||||
import at.mocode.desktop.screens.DesktopMainLayout
|
||||
import at.mocode.desktop.screens.layout.DesktopMainLayout
|
||||
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
||||
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package at.mocode.desktop
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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
|
||||
*
|
||||
* Starten mit:
|
||||
* ./gradlew :frontend:shells:meldestelle-desktop:hotRunJvm
|
||||
*
|
||||
* Einfach den gewünschten Screen in `PreviewContent()` eintragen,
|
||||
* Gradle-Task starten – Änderungen werden live ohne Neustart übernommen.
|
||||
* Bei singleWindowApplication ist kein DevelopmentEntryPoint-Wrapper nötig.
|
||||
*/
|
||||
fun main() = singleWindowApplication(title = "🔥 Hot-Reload Preview") {
|
||||
PreviewContent()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewContent() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
||||
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
||||
// VeranstalterNeuScreen(onBack = {}, onSave = {})
|
||||
// VeranstalterDetailScreen(veranstalterId = 1L, onBack = {}, onVeranstaltungSelected = {})
|
||||
// VeranstaltungUebersichtScreen(veranstalterId = 1L, onBack = {}, onTurnierSelected = {})
|
||||
// TurnierDetailScreen(turnierId = 1L, onBack = {})
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Standard: AdminUebersichtScreen (Startseite nach Login)
|
||||
AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = {},
|
||||
onVeranstaltungOeffnen = {},
|
||||
onPingService = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
package at.mocode.desktop.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Detailansicht eines Turniers gemäß Vision_03.
|
||||
*
|
||||
* Layout: Horizontale Tab-Bar mit 8 Tabs (kein eigener Toolbar-Zurück-Button –
|
||||
* Navigation erfolgt über den Breadcrumb in der TopBar).
|
||||
*
|
||||
* Tabs:
|
||||
* 1. STAMMDATEN – Turnier-Konfiguration, ZNS-Import, Sparten, Datum
|
||||
* 2. ORGANISATION – Funktionäre, Richterkollegium, Austragungsplätze
|
||||
* 3. BEWERBE – 3-spaltiges Layout (Aktionen | Tabelle | Detail-Panel)
|
||||
* 4. ARTIKEL – Gebühren, Stallungen & Boxen, Zusatzgebühren
|
||||
* 5. ABRECHNUNG – Buchungen, Offene Posten, Rechnung
|
||||
* 6. NENNUNGEN – Pferd+Reiter-Suche, Verkauf/Buchungen, Bewerbsübersicht
|
||||
* 7. STARTLISTEN – Bewerbs-Tabs, Sortierung, Zeit/Dauer
|
||||
* 8. ERGEBNISLISTEN – Bewerbs-Tabs, Platzierung & Geldpreise
|
||||
*
|
||||
* TODO: Echte Inhalte pro Tab implementieren (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun TurnierDetailScreen(
|
||||
veranstaltungId: Long,
|
||||
turnierId: Long,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
|
||||
val tabs = listOf(
|
||||
"STAMMDATEN",
|
||||
"ORGANISATION",
|
||||
"BEWERBE",
|
||||
"ARTIKEL",
|
||||
"ABRECHNUNG",
|
||||
"NENNUNGEN",
|
||||
"STARTLISTEN",
|
||||
"ERGEBNISLISTEN",
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Horizontale Tab-Bar (direkt unter der TopBar)
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
edgePadding = 0.dp,
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Tab-Inhalte
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
when (selectedTab) {
|
||||
0 -> StammdatenTabContent(turnierId = turnierId)
|
||||
1 -> OrganisationTabContent()
|
||||
2 -> BewerbeTabContent()
|
||||
3 -> ArtikelTabContent()
|
||||
4 -> AbrechnungTabContent()
|
||||
5 -> NennungenTabContent()
|
||||
6 -> StartlistenTabContent()
|
||||
7 -> ErgebnislistenTabContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab-Inhalte (Placeholder – werden in späteren Phasen befüllt)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
private fun StammdatenTabContent(turnierId: Long) {
|
||||
PlaceholderContent(
|
||||
title = "Stammdaten – Turnier $turnierId",
|
||||
subtitle = "Turnier-Konfiguration, ZNS-Import, Sparten, Klassen, Datum …",
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrganisationTabContent() {
|
||||
PlaceholderContent(
|
||||
title = "Organisation",
|
||||
subtitle = "Funktionäre & Offizielle (C-Satz), Richterkollegium, Austragungsplätze …",
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeTabContent() {
|
||||
// Typ C: 3-spaltiges Layout (Aktionen | Tabelle | Detail-Panel)
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
// Linke Aktions-Spalte
|
||||
BewerbeAktionsSpalte(modifier = Modifier.width(140.dp).fillMaxHeight())
|
||||
VerticalDivider()
|
||||
// Mittlere Tabelle
|
||||
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Bewerbe",
|
||||
subtitle = "Liste aller Bewerbe dieses Turniers …",
|
||||
)
|
||||
}
|
||||
VerticalDivider()
|
||||
// Rechtes Detail-Panel
|
||||
BewerbeDetailPanel(modifier = Modifier.width(320.dp).fillMaxHeight())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeAktionsSpalte(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
AktionsButton("Änderungen\nSpeichern")
|
||||
AktionsButton("Änderungen\nRückgängig")
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AktionsButton("Bewerb\nEinfügen")
|
||||
AktionsButton("Bewerb\nLöschen")
|
||||
AktionsButton("Bewerb Teilen")
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AktionsButton("Bewerb nach\noben verschieben")
|
||||
AktionsButton("Bewerb nach\nunten verschieben")
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
AktionsButton("Startliste\nBearbeiten")
|
||||
AktionsButton("Startliste\nDrucken")
|
||||
AktionsButton("Ergebnisliste\nBearbeiten")
|
||||
AktionsButton("Ergebnisliste\nDrucken")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AktionsButton(label: String) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 2.dp),
|
||||
) {
|
||||
Text(label, fontSize = 11.sp, lineHeight = 13.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BewerbeDetailPanel(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.padding(12.dp)) {
|
||||
// Sub-Tabs: Bewerb | Bewertung | Geldpreise | Ort/Zeit
|
||||
var subTab by remember { mutableIntStateOf(0) }
|
||||
val subTabs = listOf("Bewerb", "Bewertung", "Geldpreise", "Ort/Zeit")
|
||||
TabRow(
|
||||
selectedTabIndex = subTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
) {
|
||||
subTabs.forEachIndexed { i, title ->
|
||||
Tab(
|
||||
selected = subTab == i,
|
||||
onClick = { subTab = i },
|
||||
text = { Text(title, fontSize = 12.sp) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
PlaceholderContent(
|
||||
title = subTabs[subTab],
|
||||
subtitle = "Bewerb-Details …",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtikelTabContent() {
|
||||
PlaceholderContent(
|
||||
title = "Artikel – Nennungen & Gebühren",
|
||||
subtitle = "Nenngebühr, Startgebühr, Sporteuro, Stallungen & Boxen, Zusatzgebühren …",
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AbrechnungTabContent() {
|
||||
PlaceholderContent(
|
||||
title = "Abrechnung",
|
||||
subtitle = "Buchungen, Offene Posten, Rechnung …",
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NennungenTabContent() {
|
||||
// Typ B: 2-spaltig (Pferd+Reiter-Suche | Verkauf/Buchungen)
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Nennungen",
|
||||
subtitle = "Pferd- und Reiter-Suche, Nennungs-Tabelle …",
|
||||
)
|
||||
}
|
||||
VerticalDivider()
|
||||
Box(modifier = Modifier.width(340.dp).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Verkauf / Buchungen",
|
||||
subtitle = "Artikel-Buchungen, Bewerbsübersicht …",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartlistenTabContent() {
|
||||
// Typ B: Tabelle + rechtes Sortier/Zeit-Panel
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Startlisten",
|
||||
subtitle = "Bewerbs-Tabs, Starter-Liste …",
|
||||
)
|
||||
}
|
||||
VerticalDivider()
|
||||
Box(modifier = Modifier.width(280.dp).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Sortierung & Zeit",
|
||||
subtitle = "Aufsteigend/Absteigend, Auslosung, Beginnzeit, Reitdauer …",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErgebnislistenTabContent() {
|
||||
// Typ B: Tabelle + rechtes Platzierungs-Panel
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Ergebnislisten",
|
||||
subtitle = "Bewerbs-Tabs, Ergebnis-Eingabe (Fehler, Zeit) …",
|
||||
)
|
||||
}
|
||||
VerticalDivider()
|
||||
Box(modifier = Modifier.width(280.dp).fillMaxHeight()) {
|
||||
PlaceholderContent(
|
||||
title = "Platzierung & Geldpreis",
|
||||
subtitle = "Anzahl Platzierte, Geldpreis, Import/Export/Drucken …",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
package at.mocode.desktop.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val AccentBlue = Color(0xFF3B82F6)
|
||||
|
||||
/**
|
||||
* Screen: "Admin - Verwaltung / Veranstalter auswählen"
|
||||
*
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_22 / figma-entwurf_20):
|
||||
* - Tabelle aller registrierten Veranstalter/Kunden
|
||||
* - Klick auf Zeile → Veranstalter markiert (selektiert)
|
||||
* - "Weiter zum Veranstalter"-Button wird aktiv sobald ein Veranstalter ausgewählt ist
|
||||
*
|
||||
* TODO: Echte Daten aus customer-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstalterAuswahlScreen(
|
||||
onZurueck: () -> Unit,
|
||||
onWeiter: (Long) -> Unit,
|
||||
) {
|
||||
var selectedId by remember { mutableStateOf<Long?>(null) }
|
||||
var suchtext by remember { mutableStateOf("") }
|
||||
|
||||
// Placeholder-Daten
|
||||
val veranstalter = remember {
|
||||
listOf(
|
||||
VeranstalterUiModel(1L, "Reit- und Fahrverein Wels", "Wels", "OÖ", 12),
|
||||
VeranstalterUiModel(2L, "Pferdesportverein Linz", "Linz", "OÖ", 8),
|
||||
VeranstalterUiModel(3L, "Reiterverein Salzburg", "Salzburg", "S", 5),
|
||||
VeranstalterUiModel(4L, "Reitclub Wien Nord", "Wien", "W", 3),
|
||||
VeranstalterUiModel(5L, "Fahrverein Graz", "Graz", "ST", 7),
|
||||
)
|
||||
}
|
||||
|
||||
val gefiltert = veranstalter.filter {
|
||||
suchtext.isBlank() || it.name.contains(suchtext, ignoreCase = true) ||
|
||||
it.ort.contains(suchtext, ignoreCase = true)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Seiten-Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = "Wähle einen registrierten Veranstalter aus, um eine neue Veranstaltung anzulegen.",
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = onZurueck) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
Button(
|
||||
onClick = { selectedId?.let { onWeiter(it) } },
|
||||
enabled = selectedId != null,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Text("Weiter zum Veranstalter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Suchfeld
|
||||
OutlinedTextField(
|
||||
value = suchtext,
|
||||
onValueChange = { suchtext = it },
|
||||
placeholder = { Text("Suche nach Name oder Ort...", fontSize = 13.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Tabellen-Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFFF3F4F6))
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text("Name", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(3f))
|
||||
Text("Ort", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
|
||||
Text("Bundesland", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
Text("Veranstaltungen", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Tabellen-Inhalt
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(gefiltert) { v ->
|
||||
val isSelected = v.id == selectedId
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
if (isSelected) AccentBlue.copy(alpha = 0.1f)
|
||||
else Color.Transparent
|
||||
)
|
||||
.clickable { selectedId = v.id }
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Auswahl-Indikator
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = { selectedId = v.id },
|
||||
colors = RadioButtonDefaults.colors(selectedColor = AccentBlue),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = v.name,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
modifier = Modifier.weight(3f),
|
||||
)
|
||||
Text(v.ort, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
|
||||
Text(v.bundesland, fontSize = 13.sp, modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${v.veranstaltungsAnzahl}",
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI-Modell ---
|
||||
|
||||
data class VeranstalterUiModel(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val ort: String,
|
||||
val bundesland: String,
|
||||
val veranstaltungsAnzahl: Int,
|
||||
)
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
package at.mocode.desktop.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val StatusVorbereitungColor = Color(0xFFEA580C)
|
||||
private val StatusLiveColor = Color(0xFF16A34A)
|
||||
private val StatusAbgeschlossenColor = Color(0xFF6B7280)
|
||||
|
||||
/**
|
||||
* Screen: "Admin - Verwaltung / Veranstalter auswählen / <Vereinsname>"
|
||||
*
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_19):
|
||||
* - Veranstalter-Profil (Name, Ort, Kontakt)
|
||||
* - Liste aller Veranstaltungen dieses Veranstalters
|
||||
* - Klick auf Veranstaltung → VeranstaltungUebersicht
|
||||
*
|
||||
* TODO: Echte Daten aus customer-context / event-management-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstalterDetailScreen(
|
||||
veranstalterId: Long,
|
||||
onZurueck: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onVeranstaltungNeu: () -> Unit,
|
||||
) {
|
||||
// Placeholder-Daten
|
||||
val veranstalter = remember(veranstalterId) {
|
||||
VeranstalterUiModel(
|
||||
id = veranstalterId,
|
||||
name = "Reit- und Fahrverein Wels",
|
||||
ort = "Wels",
|
||||
bundesland = "OÖ",
|
||||
veranstaltungsAnzahl = 12,
|
||||
)
|
||||
}
|
||||
|
||||
val veranstaltungen = remember(veranstalterId) {
|
||||
listOf(
|
||||
VeranstaltungUiModel(
|
||||
id = 1L, name = "Frühjahrsturnier Wels 2026", ort = "Wels", datum = "15.04.2026",
|
||||
turnierAnzahl = 3, nennungen = 47, letzteAktivitaet = "heute",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
VeranstaltungUiModel(
|
||||
id = 2L, name = "Sommerturnier Wels 2025", ort = "Wels", datum = "20.07.2025",
|
||||
turnierAnzahl = 5, nennungen = 112, letzteAktivitaet = "vor 8 Monaten",
|
||||
status = VeranstaltungStatus.ABGESCHLOSSEN,
|
||||
),
|
||||
VeranstaltungUiModel(
|
||||
id = 3L, name = "Herbstturnier Wels 2025", ort = "Wels", datum = "12.10.2025",
|
||||
turnierAnzahl = 4, nennungen = 89, letzteAktivitaet = "vor 5 Monaten",
|
||||
status = VeranstaltungStatus.ABGESCHLOSSEN,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Veranstalter-Profil-Header
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color(0xFFF8FAFC),
|
||||
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = veranstalter.name,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("📍 ${veranstalter.ort}, ${veranstalter.bundesland}", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("🏆 ${veranstalter.veranstaltungsAnzahl} Veranstaltungen gesamt", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = onZurueck) {
|
||||
Text("← Zurück zur Auswahl")
|
||||
}
|
||||
Button(
|
||||
onClick = onVeranstaltungNeu,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Veranstaltungs-Liste
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Veranstaltungen (${veranstaltungen.size})",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
) {
|
||||
items(veranstaltungen) { veranstaltung ->
|
||||
VeranstaltungListCard(
|
||||
veranstaltung = veranstaltung,
|
||||
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VeranstaltungListCard(
|
||||
veranstaltung: VeranstaltungUiModel,
|
||||
onOeffnen: () -> Unit,
|
||||
) {
|
||||
val statusColor = when (veranstaltung.status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> StatusVorbereitungColor
|
||||
VeranstaltungStatus.LIVE -> StatusLiveColor
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> StatusAbgeschlossenColor
|
||||
}
|
||||
val statusText = when (veranstaltung.status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung"
|
||||
VeranstaltungStatus.LIVE -> "Live"
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen"
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
|
||||
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = veranstaltung.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = statusColor.copy(alpha = 0.15f),
|
||||
border = BorderStroke(1.dp, statusColor),
|
||||
) {
|
||||
Text(
|
||||
text = statusText,
|
||||
color = statusColor,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("📋 ${veranstaltung.nennungen} Nennungen", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onOeffnen,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Text("Veranstaltung öffnen →")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
package at.mocode.desktop.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.Add
|
||||
import androidx.compose.material.icons.filled.FileDownload
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val PrimaryBlue = Color(0xFF1E3A8A)
|
||||
private val ZnsGreen = Color(0xFF16A34A)
|
||||
private val ImportOrange = Color(0xFFEA580C)
|
||||
private val ExportBlue = Color(0xFF2563EB)
|
||||
|
||||
/**
|
||||
* Screen: "Veranstaltung - Übersicht"
|
||||
*
|
||||
* Gemäß Figma Vision_03 (figma-entwurf_17):
|
||||
* - Veranstaltungs-Header (Name, Datum, Ort, Status)
|
||||
* - Liste aller Turniere dieser Veranstaltung als Cards
|
||||
* - Jede Turnier-Card hat Buttons:
|
||||
* - "Öffnen" → Meldestelle des Turniers öffnen (TurnierDetail)
|
||||
* - "Import" → Datenbank-Sicherung importieren
|
||||
* - "Export" → Datenbank-Sicherung exportieren
|
||||
* - "ZNS" → ZNS-Import für dieses Turnier starten
|
||||
*
|
||||
* Warum ZNS hier? Jedes Turnier hat seine eigene Datenbank/Kassa.
|
||||
* Der ZNS-Import muss daher turnierspezifisch sein.
|
||||
*
|
||||
* TODO: Echte Daten aus event-management-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstaltungUebersichtScreen(
|
||||
veranstalterId: Long,
|
||||
veranstaltungId: Long,
|
||||
onZurueck: () -> Unit,
|
||||
onTurnierOeffnen: (turnierId: Long) -> Unit,
|
||||
onTurnierNeu: () -> Unit,
|
||||
onZnsImport: (turnierId: Long) -> Unit,
|
||||
onDbImport: (turnierId: Long) -> Unit,
|
||||
onDbExport: (turnierId: Long) -> Unit,
|
||||
) {
|
||||
// Placeholder-Daten
|
||||
val veranstaltung = remember(veranstaltungId) {
|
||||
VeranstaltungUiModel(
|
||||
id = veranstaltungId,
|
||||
name = "Frühjahrsturnier Wels 2026",
|
||||
ort = "Wels",
|
||||
datum = "15.04.2026",
|
||||
turnierAnzahl = 3,
|
||||
nennungen = 47,
|
||||
letzteAktivitaet = "heute",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
)
|
||||
}
|
||||
|
||||
val turniere = remember(veranstaltungId) {
|
||||
listOf(
|
||||
TurnierKarteUiModel(
|
||||
id = 1L,
|
||||
nummer = 1L,
|
||||
name = "Dressurturnier",
|
||||
sparte = "Dressur",
|
||||
bewerbAnzahl = 8,
|
||||
nennungen = 24,
|
||||
status = TurnierKarteStatus.VORBEREITUNG,
|
||||
datum = "15.04.2026",
|
||||
),
|
||||
TurnierKarteUiModel(
|
||||
id = 2L,
|
||||
nummer = 2L,
|
||||
name = "Springturnier",
|
||||
sparte = "Springen",
|
||||
bewerbAnzahl = 6,
|
||||
nennungen = 18,
|
||||
status = TurnierKarteStatus.VORBEREITUNG,
|
||||
datum = "15.04.2026",
|
||||
),
|
||||
TurnierKarteUiModel(
|
||||
id = 3L,
|
||||
nummer = 3L,
|
||||
name = "Vielseitigkeitsturnier",
|
||||
sparte = "Vielseitigkeit",
|
||||
bewerbAnzahl = 4,
|
||||
nennungen = 5,
|
||||
status = TurnierKarteStatus.VORBEREITUNG,
|
||||
datum = "16.04.2026",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Veranstaltungs-Header
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color(0xFFF8FAFC),
|
||||
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = veranstaltung.name,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("📍 ${veranstaltung.ort}", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("📅 ${veranstaltung.datum}", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
Text("📋 ${veranstaltung.nennungen} Nennungen gesamt", fontSize = 13.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = onZurueck) {
|
||||
Text("← Zurück")
|
||||
}
|
||||
Button(
|
||||
onClick = onTurnierNeu,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neues Turnier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Turnier-Liste
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Turniere (${turniere.size})",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Jedes Turnier hat eine eigene Datenbank und Kassa.",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF6B7280),
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
) {
|
||||
items(turniere) { turnier ->
|
||||
TurnierKarte(
|
||||
turnier = turnier,
|
||||
onOeffnen = { onTurnierOeffnen(turnier.id) },
|
||||
onZns = { onZnsImport(turnier.id) },
|
||||
onImport = { onDbImport(turnier.id) },
|
||||
onExport = { onDbExport(turnier.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TurnierKarte(
|
||||
turnier: TurnierKarteUiModel,
|
||||
onOeffnen: () -> Unit,
|
||||
onZns: () -> Unit,
|
||||
onImport: () -> Unit,
|
||||
onExport: () -> Unit,
|
||||
) {
|
||||
val statusColor = when (turnier.status) {
|
||||
TurnierKarteStatus.VORBEREITUNG -> Color(0xFFEA580C)
|
||||
TurnierKarteStatus.LIVE -> Color(0xFF16A34A)
|
||||
TurnierKarteStatus.ABGESCHLOSSEN -> Color(0xFF6B7280)
|
||||
}
|
||||
val statusText = when (turnier.status) {
|
||||
TurnierKarteStatus.VORBEREITUNG -> "Vorbereitung"
|
||||
TurnierKarteStatus.LIVE -> "Live"
|
||||
TurnierKarteStatus.ABGESCHLOSSEN -> "Abgeschlossen"
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Turnier-Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
// Turnier-Nummer Badge
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = PrimaryBlue,
|
||||
) {
|
||||
Text(
|
||||
text = "T${turnier.nummer}",
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = turnier.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🏇 ${turnier.sparte}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("📅 ${turnier.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("${turnier.bewerbAnzahl} Bewerbe", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("${turnier.nennungen} Nennungen", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Status-Badge
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = statusColor.copy(alpha = 0.15f),
|
||||
border = BorderStroke(1.dp, statusColor),
|
||||
) {
|
||||
Text(
|
||||
text = statusText,
|
||||
color = statusColor,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
HorizontalDivider(color = Color(0xFFE5E7EB))
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// Aktions-Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Öffnen – Hauptaktion
|
||||
Button(
|
||||
onClick = onOeffnen,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FolderOpen,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Öffnen", fontSize = 13.sp)
|
||||
}
|
||||
|
||||
// ZNS-Import – turnierspezifisch (eigene DB!)
|
||||
OutlinedButton(
|
||||
onClick = onZns,
|
||||
border = BorderStroke(1.dp, ZnsGreen),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Text("ZNS", fontSize = 13.sp, color = ZnsGreen, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
// Import / Export – DB-Sicherung
|
||||
OutlinedButton(
|
||||
onClick = onImport,
|
||||
border = BorderStroke(1.dp, ImportOrange),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FileUpload,
|
||||
contentDescription = null,
|
||||
tint = ImportOrange,
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Import", fontSize = 13.sp, color = ImportOrange)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onExport,
|
||||
border = BorderStroke(1.dp, ExportBlue),
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FileDownload,
|
||||
contentDescription = null,
|
||||
tint = ExportBlue,
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Export", fontSize = 13.sp, color = ExportBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI-Modelle ---
|
||||
|
||||
data class TurnierKarteUiModel(
|
||||
val id: Long,
|
||||
val nummer: Long,
|
||||
val name: String,
|
||||
val sparte: String,
|
||||
val bewerbAnzahl: Int,
|
||||
val nennungen: Int,
|
||||
val status: TurnierKarteStatus,
|
||||
val datum: String,
|
||||
)
|
||||
|
||||
enum class TurnierKarteStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.desktop.screens
|
||||
package at.mocode.desktop.screens.layout
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -15,6 +15,19 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
||||
import at.mocode.turnier.feature.presentation.TurnierNeuScreen
|
||||
import at.mocode.zns.feature.presentation.StammdatenImportScreen
|
||||
import at.mocode.ping.feature.presentation.PingScreen
|
||||
import org.koin.compose.koinInject
|
||||
import at.mocode.ping.feature.presentation.PingViewModel
|
||||
|
||||
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||
private val TopBarColor = Color(0xFF1E3A8A)
|
||||
|
|
@ -55,9 +68,9 @@ fun DesktopMainLayout(
|
|||
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
|
||||
*
|
||||
* Breadcrumb-Logik:
|
||||
* - Root: "🏠 Admin - Verwaltung"
|
||||
* - Root: "🏠 Admin - Verwaltung"
|
||||
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
|
||||
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
||||
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopTopBar(
|
||||
|
|
@ -107,6 +120,23 @@ private fun DesktopTopBar(
|
|||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstalterNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neuer Veranstalter",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -199,6 +229,15 @@ private fun DesktopTopBar(
|
|||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Ping -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Ping Service",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
@ -236,12 +275,19 @@ private fun DesktopContentArea(
|
|||
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||
onPingService = { onNavigate(AppScreen.Ping) },
|
||||
)
|
||||
|
||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
|
||||
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||
)
|
||||
|
||||
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
||||
onAbbrechen = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onSpeichern = { _, _, _ -> onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
)
|
||||
is AppScreen.VeranstalterDetail -> VeranstalterDetailScreen(
|
||||
veranstalterId = currentScreen.veranstalterId,
|
||||
|
|
@ -288,6 +334,15 @@ private fun DesktopContentArea(
|
|||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
|
||||
// Ping-Screen
|
||||
is AppScreen.Ping -> {
|
||||
val pingViewModel: PingViewModel = koinInject()
|
||||
PingScreen(
|
||||
viewModel = pingViewModel,
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback → Root
|
||||
else -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package at.mocode.desktop.screens.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
||||
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
||||
import at.mocode.turnier.feature.presentation.StammdatenTabContent
|
||||
import at.mocode.turnier.feature.presentation.OrganisationTabContent
|
||||
import at.mocode.turnier.feature.presentation.BewerbeTabContent
|
||||
import at.mocode.turnier.feature.presentation.ArtikelTabContent
|
||||
import at.mocode.turnier.feature.presentation.AbrechnungTabContent
|
||||
import at.mocode.turnier.feature.presentation.NennungenTabContent
|
||||
import at.mocode.turnier.feature.presentation.StartlistenTabContent
|
||||
import at.mocode.turnier.feature.presentation.ErgebnislistenTabContent
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Compose Desktop Previews – alle wichtigen Screens auf einen Blick
|
||||
//
|
||||
// Verwendung: In IntelliJ IDEA / Android Studio die @Preview-Funktion öffnen
|
||||
// und auf das Preview-Icon in der Gutter klicken.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Veranstalter-Auswahl ─────────────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewVeranstalterAuswahlScreen() {
|
||||
MaterialTheme {
|
||||
VeranstalterAuswahlScreen(
|
||||
onZurueck = {},
|
||||
onWeiter = {},
|
||||
onNeuerVeranstalter = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Neuer Veranstalter ───────────────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewVeranstalterNeuScreen() {
|
||||
MaterialTheme {
|
||||
VeranstalterNeuScreen(
|
||||
onAbbrechen = {},
|
||||
onSpeichern = { _, _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Veranstalter-Detail ──────────────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewVeranstalterDetailScreen() {
|
||||
MaterialTheme {
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = 1L,
|
||||
onZurueck = {},
|
||||
onVeranstaltungOeffnen = {},
|
||||
onVeranstaltungNeu = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Veranstaltung-Übersicht ──────────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewVeranstaltungUebersichtScreen() {
|
||||
MaterialTheme {
|
||||
VeranstaltungUebersichtScreen(
|
||||
veranstalterId = 1L,
|
||||
veranstaltungId = 1L,
|
||||
onZurueck = {},
|
||||
onTurnierOeffnen = {},
|
||||
onTurnierNeu = {},
|
||||
onZnsImport = {},
|
||||
onDbImport = {},
|
||||
onDbExport = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Turnier-Detail (alle Tabs) ───────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierDetailScreen() {
|
||||
MaterialTheme {
|
||||
TurnierDetailScreen(
|
||||
veranstaltungId = 1L,
|
||||
turnierId = 1L,
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Turnier-Tabs einzeln ─────────────────────────────────────────────────────
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierStammdatenTab() {
|
||||
MaterialTheme {
|
||||
StammdatenTabContent(turnierId = 1L)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierOrganisationTab() {
|
||||
MaterialTheme {
|
||||
OrganisationTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierBewerbeTab() {
|
||||
MaterialTheme {
|
||||
BewerbeTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierArtikelTab() {
|
||||
MaterialTheme {
|
||||
ArtikelTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierAbrechnungTab() {
|
||||
MaterialTheme {
|
||||
AbrechnungTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierNennungenTab() {
|
||||
MaterialTheme {
|
||||
NennungenTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierStartlistenTab() {
|
||||
MaterialTheme {
|
||||
StartlistenTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTurnierErgebnislistenTab() {
|
||||
MaterialTheme {
|
||||
ErgebnislistenTabContent()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stammdaten-Import ────────────────────────────────────────────────────────
|
||||
|
||||
// StammdatenImportScreen benötigt einen ZnsImportViewModel (Koin) – Preview nur als Hinweis.
|
||||
// @Preview
|
||||
// @Composable
|
||||
// fun PreviewStammdatenImportScreen() {
|
||||
// MaterialTheme { StammdatenImportScreen() }
|
||||
// }
|
||||
|
|
@ -50,7 +50,7 @@ springDependencyManagement = "1.1.7"
|
|||
springdoc = "3.0.0"
|
||||
|
||||
# Server Persistence
|
||||
# Final release 1.0.0 (UUID API refinements vs rc-4)
|
||||
# Final release 1.0.0 (UUID API refinements vs. rc-4)
|
||||
exposed = "1.0.0"
|
||||
postgresql = "42.7.8"
|
||||
hikari = "7.0.2"
|
||||
|
|
@ -126,6 +126,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
|
|||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
ui-desktop = { module = "androidx.compose.ui:ui-desktop", version.ref = "uiDesktop" }
|
||||
composeHotReloadApi = { module = "org.jetbrains.compose.hot-reload:hot-reload-runtime-api", version.ref = "composeHotReload" }
|
||||
|
||||
# ==============================================================================
|
||||
# === FRONTEND: NETWORK (KTOR CLIENT) ===
|
||||
|
|
|
|||
|
|
@ -135,6 +135,9 @@ include(":frontend:core:sync")
|
|||
include(":frontend:features:ping-feature")
|
||||
include(":frontend:features:nennung-feature")
|
||||
include(":frontend:features:zns-import-feature")
|
||||
include(":frontend:features:veranstalter-feature")
|
||||
include(":frontend:features:veranstaltung-feature")
|
||||
include(":frontend:features:turnier-feature")
|
||||
|
||||
// --- SHELLS ---
|
||||
include(":frontend:shells:meldestelle-desktop")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user