chore: remove obsolete screens from meldestelle-desktop module
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Failing after 2m56s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Failing after 3m3s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 2m49s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 2m13s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Failing after 2m56s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Failing after 3m3s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 2m49s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 2m13s
- Deleted unused screens including `AdminUebersichtScreen`, `AktorScreens`, `StammdatenImportScreen`, `TurnierDetailScreen`, and supporting components such as `PlaceholderContent`. - Cleaned up references and placeholders to streamline module structure. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -18,7 +18,7 @@ env:
|
|||||||
REGISTRY_INTERNAL: 10.0.0.22:3000
|
REGISTRY_INTERNAL: 10.0.0.22:3000
|
||||||
IMAGE_PREFIX: mocode-software/meldestelle
|
IMAGE_PREFIX: mocode-software/meldestelle
|
||||||
JAVA_VERSION: "25"
|
JAVA_VERSION: "25"
|
||||||
GRADLE_VERSION: "9.3.1"
|
GRADLE_VERSION: "9.4.0"
|
||||||
KEYCLOAK_IMAGE_TAG: "26.5.5"
|
KEYCLOAK_IMAGE_TAG: "26.5.5"
|
||||||
# Workers auf 4 limitiert: verhindert OOM auf dem 16GB Runner (VM 102)
|
# Workers auf 4 limitiert: verhindert OOM auf dem 16GB Runner (VM 102)
|
||||||
GRADLE_OPTS: "-Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4"
|
GRADLE_OPTS: "-Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4"
|
||||||
|
|||||||
@@ -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)*
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(compose.ui)
|
implementation(compose.ui)
|
||||||
implementation(compose.components.resources)
|
implementation(compose.components.resources)
|
||||||
implementation(libs.bundles.kmp.common)
|
implementation(libs.bundles.kmp.common)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.frontend.core.designsystem.models
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
+38
@@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -20,6 +20,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
|
|
||||||
// Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
// Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
||||||
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
||||||
|
data object VeranstalterNeu : AppScreen("/veranstalter/neu")
|
||||||
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||||
data class VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) :
|
data class VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ kotlin {
|
|||||||
|
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
|
implementation(compose.uiTooling)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsMain.dependencies {
|
jsMain.dependencies {
|
||||||
|
|||||||
+2
-2
@@ -33,13 +33,13 @@ data class PingUiState(
|
|||||||
val logs: List<LogEntry> = emptyList()
|
val logs: List<LogEntry> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
class PingViewModel(
|
open class PingViewModel(
|
||||||
private val apiClient: PingApi,
|
private val apiClient: PingApi,
|
||||||
private val syncService: PingSyncService
|
private val syncService: PingSyncService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var uiState by mutableStateOf(PingUiState())
|
var uiState by mutableStateOf(PingUiState())
|
||||||
private set
|
internal set
|
||||||
|
|
||||||
private fun addLog(source: String, message: String, isError: Boolean = false) {
|
private fun addLog(source: String, message: String, isError: Boolean = false) {
|
||||||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
|||||||
+109
@@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder-Screens für Akteur-Verwaltung (actor-context).
|
* Placeholder-Screens für Akteur-Verwaltung (actor-context).
|
||||||
+277
@@ -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)
|
||||||
+263
@@ -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)
|
||||||
+644
@@ -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
|
||||||
|
),
|
||||||
|
)
|
||||||
+93
@@ -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()
|
||||||
+215
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+251
@@ -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()
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formular zum Anlegen eines neuen Turniers (Vision_03: /veranstaltung/{id}/turnier/neu).
|
* Formular zum Anlegen eines neuen Turniers (Vision_03: /veranstaltung/{id}/turnier/neu).
|
||||||
+336
@@ -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)
|
||||||
+261
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+259
@@ -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,
|
||||||
|
)
|
||||||
+379
@@ -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,
|
||||||
|
)
|
||||||
+231
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.*
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||||
|
|
||||||
// Status-Farben gemäß Vision_03
|
// Status-Farben gemäß Vision_03
|
||||||
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
||||||
@@ -35,6 +36,7 @@ private val StatusAbgeschlossen = Color(0xFF6B7280) // Grau
|
|||||||
fun AdminUebersichtScreen(
|
fun AdminUebersichtScreen(
|
||||||
onVeranstalterAuswahl: () -> Unit,
|
onVeranstalterAuswahl: () -> Unit,
|
||||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||||
|
onPingService: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
// Placeholder-Daten für die UI-Struktur
|
// Placeholder-Daten für die UI-Struktur
|
||||||
val veranstaltungen = listOf<VeranstaltungUiModel>() // leer bis Backend angebunden
|
val veranstaltungen = listOf<VeranstaltungUiModel>() // leer bis Backend angebunden
|
||||||
@@ -73,6 +75,10 @@ fun AdminUebersichtScreen(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OutlinedButton(onClick = onPingService) {
|
||||||
|
Text("🔧 Ping")
|
||||||
|
}
|
||||||
|
|
||||||
// Status-Filter Chips
|
// Status-Filter Chips
|
||||||
StatusFilterChip("Alle", selected = true)
|
StatusFilterChip("Alle", selected = true)
|
||||||
StatusFilterChip("Vorbereitung", selected = false)
|
StatusFilterChip("Vorbereitung", selected = false)
|
||||||
@@ -345,4 +351,3 @@ data class TurnierUiModel(
|
|||||||
val bewerbAnzahl: Int,
|
val bewerbAnzahl: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class VeranstaltungStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
|
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detailansicht einer bestehenden Veranstaltung (Vision_03: /veranstaltung/{id}).
|
* Detailansicht einer bestehenden Veranstaltung (Vision_03: /veranstaltung/{id}).
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
||||||
+370
@@ -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,
|
||||||
|
)
|
||||||
|
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Veranstaltungs-Übersicht (Drawer-Einstieg gemäß Vision_03).
|
* Veranstaltungs-Übersicht (Drawer-Einstieg gemäß Vision_03).
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Feature-Modul: ZNS-Stammdaten-Import (Desktop-only)
|
* 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 {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
}
|
}
|
||||||
group = "at.mocode.clients"
|
group = "at.mocode.clients"
|
||||||
@@ -13,8 +14,17 @@ kotlin {
|
|||||||
jvm()
|
jvm()
|
||||||
sourceSets {
|
sourceSets {
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.network)
|
implementation(projects.frontend.core.network)
|
||||||
implementation(projects.frontend.core.auth)
|
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.bundles.kmp.common)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.zns.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -10,6 +10,7 @@ plugins {
|
|||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
id("org.jetbrains.compose.hot-reload")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -30,6 +31,9 @@ kotlin {
|
|||||||
implementation(projects.frontend.features.nennungFeature)
|
implementation(projects.frontend.features.nennungFeature)
|
||||||
implementation(projects.frontend.features.pingFeature)
|
implementation(projects.frontend.features.pingFeature)
|
||||||
implementation(projects.frontend.features.znsImportFeature)
|
implementation(projects.frontend.features.znsImportFeature)
|
||||||
|
implementation(projects.frontend.features.veranstalterFeature)
|
||||||
|
implementation(projects.frontend.features.veranstaltungFeature)
|
||||||
|
implementation(projects.frontend.features.turnierFeature)
|
||||||
|
|
||||||
// Compose Desktop
|
// Compose Desktop
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
@@ -39,6 +43,7 @@ kotlin {
|
|||||||
implementation(compose.ui)
|
implementation(compose.ui)
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(compose.uiTooling)
|
implementation(compose.uiTooling)
|
||||||
|
implementation(libs.composeHotReloadApi)
|
||||||
|
|
||||||
// DI (Koin)
|
// DI (Koin)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
@@ -59,6 +64,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "at.mocode.desktop.MainKt"
|
mainClass = "at.mocode.desktop.MainKt"
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import at.mocode.desktop.navigation.DesktopNavigationPort
|
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.data.AuthTokenManager
|
||||||
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
||||||
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
||||||
|
|||||||
+50
@@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-265
@@ -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 …",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-172
@@ -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,
|
|
||||||
)
|
|
||||||
-213
@@ -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 →")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-349
@@ -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 }
|
|
||||||
+56
-1
@@ -1,4 +1,4 @@
|
|||||||
package at.mocode.desktop.screens
|
package at.mocode.desktop.screens.layout
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.frontend.core.navigation.AppScreen
|
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)
|
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||||
private val TopBarColor = Color(0xFF1E3A8A)
|
private val TopBarColor = Color(0xFF1E3A8A)
|
||||||
@@ -107,6 +120,23 @@ private fun DesktopTopBar(
|
|||||||
fontSize = 14.sp,
|
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 -> {
|
is AppScreen.VeranstalterDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -199,6 +229,15 @@ private fun DesktopTopBar(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AppScreen.Ping -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Ping Service",
|
||||||
|
color = TopBarTextColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,12 +275,19 @@ private fun DesktopContentArea(
|
|||||||
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
||||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||||
|
onPingService = { onNavigate(AppScreen.Ping) },
|
||||||
)
|
)
|
||||||
|
|
||||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
|
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
|
||||||
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
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(
|
is AppScreen.VeranstalterDetail -> VeranstalterDetailScreen(
|
||||||
veranstalterId = currentScreen.veranstalterId,
|
veranstalterId = currentScreen.veranstalterId,
|
||||||
@@ -288,6 +334,15 @@ private fun DesktopContentArea(
|
|||||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Ping-Screen
|
||||||
|
is AppScreen.Ping -> {
|
||||||
|
val pingViewModel: PingViewModel = koinInject()
|
||||||
|
PingScreen(
|
||||||
|
viewModel = pingViewModel,
|
||||||
|
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback → Root
|
// Fallback → Root
|
||||||
else -> AdminUebersichtScreen(
|
else -> AdminUebersichtScreen(
|
||||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
+175
@@ -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() }
|
||||||
|
// }
|
||||||
@@ -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-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" }
|
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" }
|
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) ===
|
# === FRONTEND: NETWORK (KTOR CLIENT) ===
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ include(":frontend:core:sync")
|
|||||||
include(":frontend:features:ping-feature")
|
include(":frontend:features:ping-feature")
|
||||||
include(":frontend:features:nennung-feature")
|
include(":frontend:features:nennung-feature")
|
||||||
include(":frontend:features:zns-import-feature")
|
include(":frontend:features:zns-import-feature")
|
||||||
|
include(":frontend:features:veranstalter-feature")
|
||||||
|
include(":frontend:features:veranstaltung-feature")
|
||||||
|
include(":frontend:features:turnier-feature")
|
||||||
|
|
||||||
// --- SHELLS ---
|
// --- SHELLS ---
|
||||||
include(":frontend:shells:meldestelle-desktop")
|
include(":frontend:shells:meldestelle-desktop")
|
||||||
|
|||||||
Reference in New Issue
Block a user