Compare commits
2 Commits
b8e5065d6a
...
a72953cea7
| Author | SHA1 | Date | |
|---|---|---|---|
| a72953cea7 | |||
| 49e97915e8 |
|
|
@ -65,6 +65,7 @@ RUN mkdir -p \
|
|||
frontend/shared \
|
||||
frontend/shells/meldestelle-portal \
|
||||
frontend/shells/meldestelle-desktop \
|
||||
frontend/features/zns-import-feature \
|
||||
docs
|
||||
|
||||
# Copy root build configuration
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ RUN mkdir -p \
|
|||
frontend/shared \
|
||||
frontend/shells/meldestelle-portal \
|
||||
frontend/shells/meldestelle-desktop \
|
||||
frontend/features/zns-import-feature \
|
||||
docs
|
||||
|
||||
# Copy root build configuration
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package at.mocode.ping.api
|
||||
|
||||
interface PingApi {
|
||||
suspend fun simplePing(): PingResponse
|
||||
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
||||
suspend fun healthCheck(): HealthResponse
|
||||
suspend fun simplePing(): PingResponse
|
||||
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
||||
suspend fun healthCheck(): HealthResponse
|
||||
|
||||
// Neue Endpunkte für Security Hardening
|
||||
suspend fun publicPing(): PingResponse
|
||||
suspend fun securePing(): PingResponse
|
||||
// Neue Endpunkte für Security Hardening
|
||||
suspend fun publicPing(): PingResponse
|
||||
suspend fun securePing(): PingResponse
|
||||
|
||||
// Phase 3: Delta-Sync
|
||||
// Changed parameter name to 'since' to match SyncManager convention and backend controller
|
||||
suspend fun syncPings(since: Long): List<PingEvent>
|
||||
// Phase 3: Delta-Sync
|
||||
// Changed parameter name to 'since' to match SyncManager convention and backend controller
|
||||
suspend fun syncPings(since: Long): List<PingEvent>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ data class PingResponse(val status: String, val timestamp: String, val service:
|
|||
|
||||
@Serializable
|
||||
data class EnhancedPingResponse(
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val circuitBreakerState: String,
|
||||
val responseTime: Long
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val circuitBreakerState: String,
|
||||
val responseTime: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HealthResponse(
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val healthy: Boolean
|
||||
val status: String,
|
||||
val timestamp: String,
|
||||
val service: String,
|
||||
val healthy: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -31,6 +31,6 @@ data class PingEvent(
|
|||
// Using a String for the ID to be compatible with UUIDs from the backend.
|
||||
override val id: String,
|
||||
val message: String,
|
||||
// Using a Long for the timestamp, which can be derived from a UUIDv7.
|
||||
// Using along with the timestamp, which can be derived from a UUIDv7.
|
||||
override val lastModified: Long
|
||||
) : Syncable
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ und über definierte Schnittstellen kommunizieren.
|
|||
→ Detaillierte Planung: `docs/01_Architecture/Roadmap_ZNS_Importer.md`
|
||||
* [x] Backend-Infrastruktur & CP850 Parser (Phase 1 – Parser/Modul)
|
||||
* [x] Domain-Mapping & Upsert in DB (Phase 2)
|
||||
* [ ] REST-API & Job-Management (Phase 1 – Controller/Job-Registry)
|
||||
* [x] REST-API & Job-Management (Phase 1 – Controller/Job-Registry)
|
||||
* [ ] Frontend-Integration mit File-Picker & Status-Polling (Phase 3)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -31,4 +31,13 @@ Deine Aufgaben:
|
|||
5. Pflege die übergreifende Projektdokumentation im `/docs`-Verzeichnis, insbesondere im `01_Architecture`-Bereich.
|
||||
6. **Handover:** Stelle Architekturentscheidungen nicht nur als Text, sondern auch als Diagramm (Mermaid/PlantUML) bereit.
|
||||
7. Erstelle und pflege die MASTER ROADMAP. Du bist der "Hüter des Plans". Du delegierst Aufgaben an die spezialisierten Agenten (Backend, Frontend, DevOps, QA), führst sie aber nicht selbst aus, es sei denn, es betrifft direkt die Architektur oder das Build-System.
|
||||
|
||||
Don't:
|
||||
- Implementiere keine Business-Logik in Backend-Services (→ Backend Developer).
|
||||
- Schreibe keine UI-Komponenten oder Compose-Code (→ Frontend Expert).
|
||||
- Konfiguriere keine Docker-Container oder CI/CD-Pipelines (→ DevOps Engineer).
|
||||
- Erstelle keine Testfälle oder Teststrategien (→ QA Specialist).
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -34,3 +34,6 @@ Regeln:
|
|||
6. **Pre-Flight Check:** Prüfe vor Abschluss, ob API-Änderungen (insb. Sync) mit den Anforderungen des Frontend-Experts kompatibel sind.
|
||||
7. **Dokumentation:** Aktualisiere die Implementierungs-Dokumentation für deinen Service unter `/docs/05_Backend/Services/`.
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-25
|
||||
---
|
||||
# Playbook: Documentation & Knowledge Curator (Pflichtrolle)
|
||||
|
||||
## Beschreibung
|
||||
|
|
|
|||
|
|
@ -40,3 +40,6 @@ Arbeitsweise:
|
|||
- **Smoke Tests:** Verlasse dich nicht auf "sollte gehen". Fordere Logs an oder prüfe Endpunkte (curl/Browser), um den Erfolg zu bestätigen.
|
||||
- **Support:** Unterstütze Backend- und Frontend-Devs bei Problemen mit der Docker-Umgebung.
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -32,3 +32,6 @@ Output:
|
|||
- **Handover-Format:** Nutze **Gherkin (Given/When/Then)** für Akzeptanzkriterien, damit QA und Devs diese direkt verarbeiten können.
|
||||
- Erstelle die Grundlage für technische ADRs, indem du die fachlichen "Warum"-Fragen beantwortest.
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -33,3 +33,6 @@ Regeln:
|
|||
5. **Pre-Flight Check:** Stimme dich bei API-Anforderungen (insb. Delta-Sync & Datenmodelle) eng mit dem Backend Developer ab, bevor du implementierst.
|
||||
6. **Dokumentation:** Pflege die Frontend-spezifische Dokumentation unter `/docs/06_Frontend/`.
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ Gemini wird genutzt für **Konzeptarbeit**: Varianten vergleichen, Argumente/Tra
|
|||
|
||||
## Startpunkt
|
||||
1. `docs/README.md`
|
||||
2. `docs/03_Agents/README.md` (Artefakt-Vertrag)
|
||||
3. Je nach Thema: Architektur (`docs/01_Architecture/`), Backend (`docs/04_Backend/`), Frontend (`docs/05_Frontend/`), Infrastruktur (`docs/06_Infrastructure/`)
|
||||
2. `docs/04_Agents/README.md` (Artefakt-Vertrag)
|
||||
3. Je nach Thema: Architektur (`docs/01_Architecture/`), Backend (`docs/05_Backend/`), Frontend (`docs/06_Frontend/`), Infrastruktur (`docs/07_Infrastructure/`)
|
||||
|
||||
## Do
|
||||
* Immer 2–4 Optionen mit Vor-/Nachteilen liefern.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ Junie wird genutzt für **Repo-nahe Arbeit**: Code lesen, reale Pfade/Module fin
|
|||
|
||||
## Startpunkt
|
||||
1. `docs/README.md`
|
||||
2. Relevanter Bereich (z.B. `docs/01_Architecture/`, `docs/04_Backend/`, `docs/05_Frontend/`)
|
||||
3. Bei Rollen/Prozessfragen: `docs/03_Agents/README.md`
|
||||
2. Relevanter Bereich (z.B. `docs/01_Architecture/`, `docs/05_Backend/`, `docs/06_Frontend/`)
|
||||
3. Bei Rollen/Prozessfragen: `docs/04_Agents/README.md`
|
||||
|
||||
## Do
|
||||
* Immer mit **konkreten Dateipfaden** arbeiten.
|
||||
|
|
|
|||
|
|
@ -29,3 +29,6 @@ Regeln:
|
|||
4. Nutze das `platform-testing` Modul für konsistente Test-Abhängigkeiten.
|
||||
5. **Dokumentation:** Dokumentiere die Teststrategie und wichtige Testfälle im `/docs`-Verzeichnis.
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -32,3 +32,6 @@ Regeln für deine Antworten:
|
|||
4. **Logik vor Prosa:** Wenn du dem [Backend Developer] oder [Lead Architect] hilfst, formuliere die Regeln so um, dass sie leicht in Software-Validierungen (IF/THEN, Constraints) übersetzt werden können.
|
||||
5. **Sparringspartner:** Hinterfrage Annahmen kritisch. Wenn ein vorgeschlagener Prozess gegen die ÖTO verstößt, lege sofort Veto ein.
|
||||
```
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
type: Playbook
|
||||
status: ACTIVE
|
||||
owner: Curator
|
||||
owner: Lead Architect
|
||||
role: UI/UX Designer
|
||||
last_update: 2026-01-23
|
||||
---
|
||||
|
|
@ -105,3 +105,8 @@ Column(Modifier.fillMaxSize()) {
|
|||
|
||||
* **Mit Domain Expert:** Kläre, welche Daten *wirklich* wichtig sind (Prio 1) und welche ausgeblendet werden können (Details).
|
||||
* **Mit Frontend Expert:** Liefere keine abstrakten Ideen, sondern nutze das Vokabular von Jetpack Compose (`Row`, `Column`, `Surface`, `MaterialTheme`).
|
||||
|
||||
---
|
||||
|
||||
## Abschluss (Pflicht)
|
||||
Am Ende der Session genau **ein** Artefakt gemäß `docs/04_Agents/README.md` erzeugen oder aktualisieren (ADR / Reference / How-to / Journal Entry).
|
||||
|
|
|
|||
|
|
@ -34,6 +34,16 @@ Jede KI-Session endet mit **genau einem** Artefakt in `docs/`:
|
|||
|
||||
## Playbooks
|
||||
|
||||
* `Playbooks/Junie.md`
|
||||
* `Playbooks/Gemini.md`
|
||||
* `Playbooks/Curator.md`
|
||||
| Playbook | Rolle | Typ |
|
||||
|----------|-------|-----|
|
||||
| `Playbooks/Architect.md` | 🏗️ Lead Architect | Strategie, Planung, Build-System |
|
||||
| `Playbooks/BackendDeveloper.md` | 👷 Backend Developer | Spring Boot, Kotlin, DDD |
|
||||
| `Playbooks/Curator.md` | 🧹 Curator | Dokumentation, Wissensmanagement |
|
||||
| `Playbooks/DevOpsEngineer.md` | 🐧 DevOps Engineer | Docker, CI/CD, Infrastruktur |
|
||||
| `Playbooks/DomainExpert.md` | 📋 Domain Expert | Fachlichkeit, Regelwerke, Gherkin |
|
||||
| `Playbooks/FrontendExpert.md` | 🎨 Frontend Expert | KMP, Compose, Offline-First |
|
||||
| `Playbooks/Gemini.md` | 🤖 Gemini (extern) | Konzeptarbeit, Optionen, ADR-Formulierung |
|
||||
| `Playbooks/Junie.md` | 🤖 Junie (IDE) | Repo-nahe Arbeit, Code, Implementierung |
|
||||
| `Playbooks/QASpecialist.md` | 🧐 QA Specialist | Teststrategie, Edge-Cases |
|
||||
| `Playbooks/RulebookExpert.md` | 📜 ÖTO/FEI Rulebook Expert | Regelwerks-Compliance, Validierung |
|
||||
| `Playbooks/UIUXDesigner.md` | 🖌️ UI/UX Designer | High-Density Design, Wireframes |
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 100 KiB |
42
docs/99_Journal/2026-03-25_Playbook_Audit.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-25
|
||||
---
|
||||
# Journal: Playbook Audit & Fixes
|
||||
|
||||
## Session-Ziel
|
||||
Vollständige Analyse aller Agent-Playbooks auf Konsistenz, Korrektheit und Vollständigkeit.
|
||||
|
||||
## Befunde
|
||||
|
||||
### 🔴 Kritisch (behoben)
|
||||
- **`Gemini.md` & `Junie.md`:** Falsche Pfade in der Startpunkt-Sektion.
|
||||
- `docs/03_Agents/` → korrigiert zu `docs/04_Agents/`
|
||||
- `docs/04_Backend/` → korrigiert zu `docs/05_Backend/`
|
||||
- `docs/05_Frontend/` → korrigiert zu `docs/06_Frontend/`
|
||||
- `docs/06_Infrastructure/` → korrigiert zu `docs/07_Infrastructure/`
|
||||
|
||||
### ⚠️ Mittel (behoben)
|
||||
- **`Curator.md`:** Fehlender Frontmatter-Header — ergänzt (`type`, `status`, `owner`, `last_update`).
|
||||
- **`04_Agents/README.md`:** Nur 3 von 11 Playbooks aufgelistet — vollständige Tabelle ergänzt.
|
||||
|
||||
### ℹ️ Nachträglich behoben (Session 2)
|
||||
- **Alle Playbooks:** `## Abschluss (Pflicht)`-Abschnitt mit Curator-Hinweis ergänzt.
|
||||
- **`UIUXDesigner.md`:** `owner: Curator` → `owner: Lead Architect` korrigiert.
|
||||
- **`Architect.md`:** Expliziter `Don't`-Block zur Rollenabgrenzung ergänzt.
|
||||
|
||||
## Geänderte Dateien
|
||||
- `docs/04_Agents/Playbooks/Gemini.md`
|
||||
- `docs/04_Agents/Playbooks/Junie.md`
|
||||
- `docs/04_Agents/Playbooks/Curator.md`
|
||||
- `docs/04_Agents/README.md`
|
||||
- `docs/04_Agents/Playbooks/Architect.md`
|
||||
- `docs/04_Agents/Playbooks/BackendDeveloper.md`
|
||||
- `docs/04_Agents/Playbooks/DevOpsEngineer.md`
|
||||
- `docs/04_Agents/Playbooks/DomainExpert.md`
|
||||
- `docs/04_Agents/Playbooks/FrontendExpert.md`
|
||||
- `docs/04_Agents/Playbooks/QASpecialist.md`
|
||||
- `docs/04_Agents/Playbooks/RulebookExpert.md`
|
||||
- `docs/04_Agents/Playbooks/UIUXDesigner.md`
|
||||
|
|
@ -17,6 +17,13 @@ sealed class AppScreen(val route: String) {
|
|||
|
||||
// --- Desktop-Navigation (Vision_03) ---
|
||||
data object Veranstaltungen : AppScreen("/veranstaltungen")
|
||||
|
||||
// Neuer Flow: + Neue Veranstaltung → Veranstalter auswählen → Veranstalter-Detail → Veranstaltung-Übersicht
|
||||
data object VeranstalterAuswahl : AppScreen("/veranstalter/auswahl")
|
||||
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||
data class VeranstaltungUebersicht(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||
|
||||
data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id")
|
||||
data object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
|
||||
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
|
||||
|
|
@ -34,6 +41,8 @@ sealed class AppScreen(val route: String) {
|
|||
private val VERANSTALTUNG_DETAIL = Regex("/veranstaltung/(\\d+)$")
|
||||
private val TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
|
||||
private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$")
|
||||
private val VERANSTALTER_DETAIL = Regex("/veranstalter/(\\d+)$")
|
||||
private val VERANSTALTUNG_UEBERSICHT = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$")
|
||||
|
||||
fun fromRoute(route: String): AppScreen {
|
||||
return when (route) {
|
||||
|
|
@ -48,6 +57,7 @@ sealed class AppScreen(val route: String) {
|
|||
"/auth/callback" -> AuthCallback
|
||||
"/nennung" -> Nennung
|
||||
"/veranstaltungen" -> Veranstaltungen
|
||||
"/veranstalter/auswahl" -> VeranstalterAuswahl
|
||||
"/veranstaltung/neu" -> VeranstaltungNeu
|
||||
"/reiter" -> Reiter
|
||||
"/pferde" -> Pferde
|
||||
|
|
@ -65,6 +75,12 @@ sealed class AppScreen(val route: String) {
|
|||
VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
|
||||
return VeranstaltungDetail(id.toLong())
|
||||
}
|
||||
VERANSTALTER_DETAIL.matchEntire(route)?.destructured?.let { (vId) ->
|
||||
return VeranstalterDetail(vId.toLong())
|
||||
}
|
||||
VERANSTALTUNG_UEBERSICHT.matchEntire(route)?.destructured?.let { (verId, vId) ->
|
||||
return VeranstaltungUebersicht(verId.toLong(), vId.toLong())
|
||||
}
|
||||
Landing // Default fallback
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@ package at.mocode.frontend.core.navigation
|
|||
|
||||
import at.mocode.frontend.core.domain.models.AppRoles
|
||||
import at.mocode.frontend.core.domain.models.User
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
private class FakeNav : NavigationPort {
|
||||
var last: String? = null
|
||||
override val currentScreen: StateFlow<AppScreen> = MutableStateFlow(AppScreen.Landing)
|
||||
override fun navigateTo(route: String) {
|
||||
last = route
|
||||
}
|
||||
override fun navigateToScreen(screen: AppScreen) {
|
||||
last = screen.route
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeUserProvider(private val user: User?) : CurrentUserProvider {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,348 @@
|
|||
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.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Status-Farben gemäß Vision_03
|
||||
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
||||
private val StatusLive = Color(0xFF16A34A) // Grün
|
||||
private val StatusAbgeschlossen = Color(0xFF6B7280) // Grau
|
||||
|
||||
/**
|
||||
* Root-Screen der Desktop-App gemäß Vision_03 (Screenshot 23/24).
|
||||
*
|
||||
* Layout:
|
||||
* - KPI-Kacheln oben (Live/Aktiv, In Vorbereitung, Gesamt, Archiv)
|
||||
* - Toolbar: "+ Neue Veranstaltung" + Suche + Status-Filter
|
||||
* - Veranstaltungs-Cards (expandiert mit Turnier-Liste)
|
||||
*
|
||||
* TODO: Echte Daten aus event-management-context laden (Phase 4/5).
|
||||
*/
|
||||
@Composable
|
||||
fun AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
) {
|
||||
// Placeholder-Daten für die UI-Struktur
|
||||
val veranstaltungen = listOf<VeranstaltungUiModel>() // leer bis Backend angebunden
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// KPI-Kacheln
|
||||
KpiKachelRow(
|
||||
liveAktiv = 0,
|
||||
inVorbereitung = 0,
|
||||
gesamt = 0,
|
||||
archiv = 0,
|
||||
)
|
||||
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = onVeranstalterAuswahl,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 13.sp) },
|
||||
modifier = Modifier.weight(1f).height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Status-Filter Chips
|
||||
StatusFilterChip("Alle", selected = true)
|
||||
StatusFilterChip("Vorbereitung", selected = false)
|
||||
StatusFilterChip("Live", selected = false)
|
||||
StatusFilterChip("Abgeschlossen", selected = false)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Veranstaltungs-Liste
|
||||
if (veranstaltungen.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "Noch keine Veranstaltungen",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Lege eine neue Veranstaltung an, um zu beginnen.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onVeranstalterAuswahl,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neue Veranstaltung anlegen")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
items(veranstaltungen) { veranstaltung ->
|
||||
VeranstaltungCard(
|
||||
veranstaltung = veranstaltung,
|
||||
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
|
||||
onLoeschen = { /* TODO */ },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpiKachelRow(
|
||||
liveAktiv: Int,
|
||||
inVorbereitung: Int,
|
||||
gesamt: Int,
|
||||
archiv: Int,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
KpiKachel(
|
||||
label = "LIVE / AKTIV",
|
||||
wert = liveAktiv.toString(),
|
||||
akzentFarbe = StatusLive,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
KpiKachel(
|
||||
label = "IN VORBEREITUNG",
|
||||
wert = inVorbereitung.toString(),
|
||||
akzentFarbe = Color(0xFF3B82F6),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
KpiKachel(
|
||||
label = "GESAMT",
|
||||
wert = gesamt.toString(),
|
||||
akzentFarbe = Color(0xFF6B7280),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
KpiKachel(
|
||||
label = "ARCHIV",
|
||||
wert = archiv.toString(),
|
||||
akzentFarbe = Color(0xFF9CA3AF),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpiKachel(
|
||||
label: String,
|
||||
wert: String,
|
||||
akzentFarbe: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
border = BorderStroke(2.dp, akzentFarbe),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
text = wert,
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = akzentFarbe,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusFilterChip(label: String, selected: Boolean) {
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = {},
|
||||
label = { Text(label, fontSize = 12.sp) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VeranstaltungCard(
|
||||
veranstaltung: VeranstaltungUiModel,
|
||||
onOeffnen: () -> Unit,
|
||||
onLoeschen: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
|
||||
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = veranstaltung.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(top = 2.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))
|
||||
}
|
||||
}
|
||||
StatusBadge(veranstaltung.status)
|
||||
}
|
||||
|
||||
// Turnier-Liste
|
||||
if (veranstaltung.turniere.isNotEmpty()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Turniere (${veranstaltung.turniere.size}):", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
veranstaltung.turniere.forEach { turnier ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = Color(0xFF1E3A8A),
|
||||
) {
|
||||
Text(
|
||||
text = turnier.nummer.toString(),
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
Text("${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
|
||||
Text("Öffnen", fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Nennungen: ${veranstaltung.nennungen}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("Letzte Aktivität: ${veranstaltung.letzteAktivitaet}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = onOeffnen,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
modifier = Modifier.height(32.dp),
|
||||
) {
|
||||
Text("Öffnen", fontSize = 12.sp)
|
||||
}
|
||||
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusBadge(status: VeranstaltungStatus) {
|
||||
val (text, color) = when (status) {
|
||||
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung" to StatusVorbereitung
|
||||
VeranstaltungStatus.LIVE -> "Live" to StatusLive
|
||||
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen" to StatusAbgeschlossen
|
||||
}
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = color.copy(alpha = 0.15f),
|
||||
border = BorderStroke(1.dp, color),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI-Modelle (Placeholder bis echte Domain-Modelle angebunden sind) ---
|
||||
|
||||
data class VeranstaltungUiModel(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val ort: String,
|
||||
val datum: String,
|
||||
val turnierAnzahl: Int,
|
||||
val nennungen: Int,
|
||||
val letzteAktivitaet: String,
|
||||
val status: VeranstaltungStatus,
|
||||
val turniere: List<TurnierUiModel> = emptyList(),
|
||||
)
|
||||
|
||||
data class TurnierUiModel(
|
||||
val id: Long,
|
||||
val nummer: Long,
|
||||
val name: String,
|
||||
val bewerbAnzahl: Int,
|
||||
)
|
||||
|
||||
enum class VeranstaltungStatus { VORBEREITUNG, LIVE, ABGESCHLOSSEN }
|
||||
|
|
@ -3,27 +3,32 @@ package at.mocode.desktop.screens
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.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.navigation.AppScreen
|
||||
import at.mocode.nennung.feature.presentation.NennungViewModel
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||
private val TopBarColor = Color(0xFF1E3A8A)
|
||||
private val TopBarTextColor = Color.White
|
||||
|
||||
/**
|
||||
* Haupt-Layout der Desktop-App gemäß Vision_03.
|
||||
* Sidebar (links) + Content-Bereich (rechts).
|
||||
*
|
||||
* Struktur:
|
||||
* - TopBar (dunkelblau): App-Titel + Breadcrumb + Logout
|
||||
* - Content: kontextabhängiger Screen
|
||||
*
|
||||
* Kein Nav-Rail, keine Sidebar – Navigation erfolgt über
|
||||
* Breadcrumb-Klicks und horizontale Tabs innerhalb der Screens.
|
||||
*/
|
||||
@Composable
|
||||
fun DesktopMainLayout(
|
||||
|
|
@ -31,16 +36,12 @@ fun DesktopMainLayout(
|
|||
onNavigate: (AppScreen) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopSidebar(
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopTopBar(
|
||||
currentScreen = currentScreen,
|
||||
onNavigate = onNavigate,
|
||||
onLogout = onLogout,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxHeight().width(1.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopContentArea(
|
||||
currentScreen = currentScreen,
|
||||
|
|
@ -50,182 +51,246 @@ fun DesktopMainLayout(
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private data class NavItem(
|
||||
val label: String,
|
||||
val icon: ImageVector,
|
||||
val screen: AppScreen,
|
||||
)
|
||||
|
||||
private val navItems = listOf(
|
||||
NavItem("Veranstaltungen", Icons.Default.Event, AppScreen.Veranstaltungen),
|
||||
NavItem("Reiter", Icons.Default.Person, AppScreen.Reiter),
|
||||
NavItem("Pferde", Icons.Default.Star, AppScreen.Pferde),
|
||||
NavItem("Funktionäre", Icons.Default.Badge, AppScreen.Funktionaere),
|
||||
NavItem("Meisterschaften", Icons.Default.EmojiEvents, AppScreen.Meisterschaften),
|
||||
NavItem("Cups", Icons.Default.WorkspacePremium, AppScreen.Cups),
|
||||
NavItem("Stammdaten-Import", Icons.Default.CloudUpload, AppScreen.StammdatenImport),
|
||||
)
|
||||
|
||||
/**
|
||||
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
|
||||
*
|
||||
* Breadcrumb-Logik:
|
||||
* - Root: "🏠 Admin - Verwaltung"
|
||||
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
|
||||
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopSidebar(
|
||||
private fun DesktopTopBar(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(220.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(vertical = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.background(TopBarColor)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
// App-Titel
|
||||
Text(
|
||||
text = "Meldestelle",
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Zurück-Pfeil (nur wenn nicht Root)
|
||||
if (currentScreen !is AppScreen.Veranstaltungen) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
tint = TopBarTextColor,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable { onNavigate(AppScreen.Veranstaltungen) },
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Navigations-Einträge
|
||||
navItems.forEach { item ->
|
||||
val isSelected = currentScreen::class == item.screen::class
|
||||
SidebarNavItem(
|
||||
item = item,
|
||||
isSelected = isSelected,
|
||||
onClick = { onNavigate(item.screen) },
|
||||
// Root-Link
|
||||
Text(
|
||||
text = "🏠 Admin - Verwaltung",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.Veranstaltungen) },
|
||||
)
|
||||
|
||||
// Breadcrumb-Segmente je nach Screen
|
||||
when (currentScreen) {
|
||||
is AppScreen.VeranstalterAuswahl -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungUebersicht -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.id}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neue Veranstaltung",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
is AppScreen.TurnierDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Turnier ${currentScreen.turnierId}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.TurnierNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neues Turnier",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Logout
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onLogout() }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Logout rechts
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||
contentDescription = "Logout",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Logout",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
contentDescription = "Abmelden",
|
||||
tint = TopBarTextColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SidebarNavItem(
|
||||
item: NavItem,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val bgColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
|
||||
val contentColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
.background(bgColor, RoundedCornerShape(8.dp))
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = item.label,
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = item.label,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
),
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
private fun BreadcrumbSeparator() {
|
||||
Text(
|
||||
text = " / ",
|
||||
color = TopBarTextColor.copy(alpha = 0.6f),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content-Bereich: Screen-Routing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen.
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopContentArea(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit,
|
||||
) {
|
||||
val nennungViewModel: NennungViewModel = koinViewModel()
|
||||
|
||||
when (currentScreen) {
|
||||
is AppScreen.Veranstaltungen -> VeranstaltungenScreen(
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
||||
// Root-Screen: Admin-Übersicht
|
||||
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||
)
|
||||
|
||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
|
||||
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||
)
|
||||
is AppScreen.VeranstalterDetail -> VeranstalterDetailScreen(
|
||||
veranstalterId = currentScreen.veranstalterId,
|
||||
onZurueck = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { vId ->
|
||||
onNavigate(AppScreen.VeranstaltungUebersicht(currentScreen.veranstalterId, vId))
|
||||
},
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId)) },
|
||||
)
|
||||
is AppScreen.VeranstaltungUebersicht -> VeranstaltungUebersichtScreen(
|
||||
veranstalterId = currentScreen.veranstalterId,
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
onZurueck = { onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId)) },
|
||||
onTurnierOeffnen = { tId ->
|
||||
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, tId))
|
||||
},
|
||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.veranstaltungId)) },
|
||||
onZnsImport = { /* TODO: ZNS-Import Dialog für Turnier */ },
|
||||
onDbImport = { /* TODO: DB-Import Dialog */ },
|
||||
onDbExport = { /* TODO: DB-Export Dialog */ },
|
||||
)
|
||||
|
||||
// Veranstaltungs-Screens
|
||||
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
|
||||
veranstaltungId = currentScreen.id,
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
|
||||
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
|
||||
)
|
||||
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onSave = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
)
|
||||
|
||||
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
|
||||
veranstaltungId = currentScreen.id,
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
|
||||
onTurnierOeffnen = { turnierId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, turnierId)) },
|
||||
// Turnier-Screens
|
||||
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
|
||||
is AppScreen.TurnierNeu -> TurnierNeuScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
|
||||
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
nennungViewModel = nennungViewModel,
|
||||
)
|
||||
|
||||
is AppScreen.Reiter -> ReiterScreen()
|
||||
is AppScreen.Pferde -> PferdeScreen()
|
||||
is AppScreen.Funktionaere -> FunktionaereScreen()
|
||||
is AppScreen.Meisterschaften -> MeisterschaftenScreen()
|
||||
is AppScreen.Cups -> CupsScreen()
|
||||
is AppScreen.StammdatenImport -> StammdatenImportScreen()
|
||||
// Fallback für alle anderen Screens (Dashboard, Ping etc.)
|
||||
else -> VeranstaltungenScreen(
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
||||
// Fallback → Root
|
||||
else -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +1,265 @@
|
|||
package at.mocode.desktop.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
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 at.mocode.nennung.feature.presentation.NennungViewModel
|
||||
import at.mocode.nennung.feature.presentation.NennungsMaske
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Detailansicht eines bestehenden Turniers (Vision_03: /veranstaltung/{id}/turnier/{tid}).
|
||||
* Tabs: Übersicht | Stammdaten (A-Satz) | Organisation | Bewerbe ⭐ | Preisliste
|
||||
* Der Bewerbe-Tab integriert die NennungsMaske aus dem nennung-feature.
|
||||
* 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,
|
||||
nennungViewModel: NennungViewModel,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(3) } // Bewerbe ist Standard-Tab (⭐)
|
||||
val tabs = listOf("Übersicht", "Stammdaten (A-Satz)", "Organisation", "Bewerbe ⭐", "Preisliste")
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
|
||||
val tabs = listOf(
|
||||
"STAMMDATEN",
|
||||
"ORGANISATION",
|
||||
"BEWERBE",
|
||||
"ARTIKEL",
|
||||
"ABRECHNUNG",
|
||||
"NENNUNGEN",
|
||||
"STARTLISTEN",
|
||||
"ERGEBNISLISTEN",
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
// Horizontale Tab-Bar (direkt unter der TopBar)
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = Color(0xFF1E3A8A),
|
||||
edgePadding = 0.dp,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Turnier #$turnierId",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
|
||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = { Text(title) },
|
||||
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 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Übersicht", "Turnier-Stammdaten und Status.")
|
||||
}
|
||||
|
||||
1 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Stammdaten (A-Satz)", "OEPS-Turniernummer, Kategorie, Sparte …")
|
||||
}
|
||||
|
||||
2 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Organisation", "Richter, Parcourschef, Tierarzt …")
|
||||
}
|
||||
|
||||
3 -> {
|
||||
// Nennungs-Workflow: NennungsMaske aus nennung-feature
|
||||
NennungsMaske(
|
||||
viewModel = nennungViewModel,
|
||||
onStartlisteOeffnen = { /* TODO: Navigation zu Startliste */ },
|
||||
onErgebnisseOeffnen = { /* TODO: Navigation zu Ergebnisse */ },
|
||||
onAbrechnungOeffnen = { /* TODO: Navigation zu Abrechnung */ },
|
||||
)
|
||||
}
|
||||
|
||||
4 -> Box(Modifier.padding(24.dp)) {
|
||||
PlaceholderContent("Preisliste", "Nenngebühren pro Bewerb/Sparte …")
|
||||
}
|
||||
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 …",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
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 →")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
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 }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
2
gradlew
vendored
|
|
@ -57,7 +57,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
|
|
|||