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

- 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:
2026-03-26 15:08:38 +01:00
parent 1d393fdefe
commit c2b3b5889f
50 changed files with 5067 additions and 1016 deletions
@@ -0,0 +1,73 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
/**
* Placeholder-Screens für Akteur-Verwaltung (actor-context).
* Werden in Phase 4/5 mit echten Daten aus dem actor-context befüllt.
*/
@Composable
fun ReiterScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Reiter", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Reiter-Verwaltung",
subtitle = "Satznummer, Lizenzklasse, Sparten-Lizenz actor-context (Phase 4).",
)
}
}
@Composable
fun PferdeScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Pferde", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Pferde-Verwaltung",
subtitle = "Lebensnummer, ZNS-Daten, Passbesitzer actor-context (Phase 4).",
)
}
}
@Composable
fun FunktionaereScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Funktionäre", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Funktionäre-Verwaltung",
subtitle = "Richter, Parcourschef, Tierarzt actor-context (Phase 4).",
)
}
}
@Composable
fun MeisterschaftenScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Meisterschaften", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Meisterschaften",
subtitle = "Konfigurierbare Reglements, Punktesysteme series-context (Phase 2+).",
)
}
}
@Composable
fun CupsScreen() {
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
Text("Cups", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(24.dp))
PlaceholderContent(
title = "Cups & Serien",
subtitle = "Pluggable Berechnungsmodell, Paar-Bindung series-context (Phase 2+).",
)
}
}
@@ -0,0 +1,277 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Print
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
private val OffenePostenRot = Color(0xFFDC2626)
/**
* ABRECHNUNG-Tab im TurnierDetailScreen.
* Gemäß Figma Vision_03 (figma-entwurf_06):
* - Sub-Tabs: BUCHUNGEN | OFFENE POSTEN | RECHNUNG
* - Rechte Sidebar: AUSWAHL | VERKAUF | BUCHUNGEN | ADRESSEN
* - Buchungstabelle: Buchungstext, Soll, Haben, Saldo, Buchen-Checkbox, Rechnung-Checkbox
* - Rechte Sidebar: Suche nach Reiter/Pferd, Zahlungsart, Buchen-Button
*/
@Composable
fun AbrechnungTabContent() {
var subTab by remember { mutableIntStateOf(0) }
var sidebarTab by remember { mutableIntStateOf(2) } // BUCHUNGEN default
val subTabs = listOf("BUCHUNGEN", "OFFENE POSTEN", "RECHNUNG")
val sidebarTabs = listOf("AUSWAHL", "VERKAUF", "BUCHUNGEN", "ADRESSEN")
// Placeholder-Buchungen
val buchungen = remember {
listOf(
BuchungspositionUiModel("Startgebühr Bewerb 12 - Dressur Kl. A", 25.00, 0.00),
BuchungspositionUiModel("Startgebühr Bewerb 15 - Springen Kl. B", 30.00, 0.00),
BuchungspositionUiModel("Nenngeld", 15.00, 0.00),
BuchungspositionUiModel("Box 3 Tage", 45.00, 0.00),
)
}
Row(modifier = Modifier.fillMaxSize()) {
// ── Hauptbereich ─────────────────────────────────────────────────────
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
// Sub-Tabs
TabRow(
selectedTabIndex = subTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = Color(0xFF1E3A8A),
) {
subTabs.forEachIndexed { i, title ->
Tab(
selected = subTab == i,
onClick = { subTab = i },
text = {
Text(
title,
fontSize = 12.sp,
fontWeight = if (subTab == i) FontWeight.Bold else FontWeight.Normal
)
},
)
}
}
when (subTab) {
0 -> BuchungenContent(buchungen)
1 -> OffenePostenContent()
2 -> RechnungContent()
}
}
VerticalDivider()
// ── Rechte Sidebar ───────────────────────────────────────────────────
Column(modifier = Modifier.width(320.dp).fillMaxHeight()) {
TabRow(
selectedTabIndex = sidebarTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = Color(0xFF1E3A8A),
) {
sidebarTabs.forEachIndexed { i, title ->
Tab(
selected = sidebarTab == i,
onClick = { sidebarTab = i },
text = { Text(title, fontSize = 11.sp) },
)
}
}
when (sidebarTab) {
2 -> BuchungenSidebar()
else -> PlaceholderContent(title = sidebarTabs[sidebarTab], subtitle = "")
}
}
}
}
@Composable
private fun BuchungenContent(buchungen: List<BuchungspositionUiModel>) {
val gesamtSoll = buchungen.sumOf { it.soll }
val gesamtHaben = buchungen.sumOf { it.haben }
val gesamtSaldo = gesamtSoll - gesamtHaben
Column(modifier = Modifier.fillMaxSize()) {
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(4.dp))
Text("Aktualisieren", fontSize = 12.sp)
}
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
Text("Übersicht", fontSize = 12.sp)
}
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
Text("Tabelle Leeren", fontSize = 12.sp, color = Color(0xFFEA580C))
}
OutlinedButton(onClick = {}, modifier = Modifier.height(36.dp)) {
Text("Pferd aus Liste entfernen", fontSize = 12.sp)
}
}
// Tabellen-Header
Row(
modifier = Modifier.fillMaxWidth().background(Color(0xFFF3F4F6)).padding(horizontal = 12.dp, vertical = 6.dp),
) {
Text("Buchungstext", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
Text("Soll", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Haben", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Saldo", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Buchen", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
Text("Rechnung", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
}
HorizontalDivider()
LazyColumn(modifier = Modifier.weight(1f)) {
items(buchungen) { b ->
val saldo = b.soll - b.haben
var buchen by remember { mutableStateOf(false) }
var rechnung by remember { mutableStateOf(false) }
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(b.buchungstext, fontSize = 13.sp, modifier = Modifier.weight(3f))
Text("%.2f €".format(b.soll), fontSize = 13.sp, modifier = Modifier.weight(1f))
Text("%.2f €".format(b.haben), fontSize = 13.sp, modifier = Modifier.weight(1f))
Text(
"%.2f €".format(saldo),
fontSize = 13.sp,
color = if (saldo > 0) OffenePostenRot else Color.Unspecified,
fontWeight = if (saldo > 0) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.weight(1f),
)
Checkbox(checked = buchen, onCheckedChange = { buchen = it }, modifier = Modifier.width(60.dp))
Checkbox(checked = rechnung, onCheckedChange = { rechnung = it }, modifier = Modifier.width(70.dp))
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
// Gesamt-Zeile
HorizontalDivider()
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text("GESAMT", fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(3f))
Text("%.2f €".format(gesamtSoll), fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
Text("%.2f €".format(gesamtHaben), fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
Text(
"%.2f €".format(gesamtSaldo),
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = if (gesamtSaldo > 0) OffenePostenRot else Color.Unspecified,
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun BuchungenSidebar() {
var suchtext by remember { mutableStateOf("") }
var zahlungsart by remember { mutableStateOf("BAR") }
Column(modifier = Modifier.fillMaxSize().padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Nach Reiter oder Pferd", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
OutlinedTextField(
value = suchtext,
onValueChange = { suchtext = it },
placeholder = { Text("Bitte auswählen...", fontSize = 12.sp) },
modifier = Modifier.fillMaxWidth().height(44.dp),
singleLine = true,
)
HorizontalDivider()
Text("Buchen:", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("0.00 €", fontSize = 22.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
Button(
onClick = {},
enabled = false,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) { Text("Buchen") }
}
HorizontalDivider()
Text("Direkt Drucken:", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) {
Icon(Icons.Default.Print, contentDescription = null, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(4.dp))
Text("Saldo", fontSize = 12.sp)
}
OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) {
Text("Rechnung", fontSize = 12.sp)
}
}
HorizontalDivider()
Text("Zahlungsart:", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
listOf("BAR", "Scheck (+30 €)", "Bankomat", "Kreditkarte").forEach { art ->
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = zahlungsart == art, onClick = { zahlungsart = art })
Text(art, fontSize = 13.sp)
}
}
Button(
onClick = {},
enabled = false,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) { Text("Gebühr buchen") }
// Hinweis
Surface(
color = Color(0xFFEFF6FF),
shape = MaterialTheme.shapes.small,
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFBFDBFE)),
) {
Text(
"💡 Hinweis: Bei Barzahlung werden die Buchungen sofort verarbeitet. Scheck-Zahlungen erfordern eine zusätzliche Gebühr von 30 €.",
fontSize = 11.sp,
color = Color(0xFF1E40AF),
modifier = Modifier.padding(8.dp),
)
}
}
}
@Composable
private fun OffenePostenContent() {
PlaceholderContent(title = "Offene Posten", subtitle = "Alle offenen Forderungen …")
}
@Composable
private fun RechnungContent() {
PlaceholderContent(title = "Rechnung", subtitle = "Rechnungserstellung …")
}
// --- UI-Modelle ---
data class BuchungspositionUiModel(val buchungstext: String, val soll: Double, val haben: Double)
@@ -0,0 +1,263 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
private val DeleteRed = Color(0xFFDC2626)
/**
* ARTIKEL-Tab im TurnierDetailScreen.
* Gemäß Figma Vision_03 (figma-entwurf_07 / figma-entwurf_08):
* - Nennungen & Gebühren: Nenngebühr, Startgebühr, Sporteuro, Nachnennungsgebühr, Nennungstausch
* - Stallungen & Boxen: Box/Tag, Einstreu, Paddock
* - Zusatzgebühren: dynamische Liste (Bezeichnung, Betrag, Pflicht)
*/
@Composable
fun ArtikelTabContent() {
var nenngebuehr by remember { mutableStateOf("0.00") }
var startgebuehr by remember { mutableStateOf("15.00") }
var sporteuro by remember { mutableStateOf("0.00") }
var nachnennungsgebuehr by remember { mutableStateOf("0.00") }
var nennungstauschGebuehr by remember { mutableStateOf("0.00") }
var boxProTag by remember { mutableStateOf("0.00") }
var einstreuErstEinstreu by remember { mutableStateOf("0.00") }
var einstreuNachlegen by remember { mutableStateOf("0.00") }
var paddockProTag by remember { mutableStateOf("0.00") }
var zusatzgebuehren by remember {
mutableStateOf(
listOf(
ZusatzgebuehrUiModel("Stromanschluss pro Tag", "5.00", false),
ZusatzgebuehrUiModel("Camping pro Nacht", "10.00", false),
),
)
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// ── Nennungen & Gebühren ─────────────────────────────────────────────
ArtikelSectionCard(title = "Nennungen & Gebühren") {
ArtikelSubSection("Nennungs- und Startgebühren") {
ArtikelFormRow("Nenngebühr pro Pferd/Reiter:", "(Grundgebühr unabhängig von Anzahl Bewerben)") {
EuroTextField(nenngebuehr) { nenngebuehr = it }
}
ArtikelFormRow("Startgebühr pro Bewerb:", "(Pro einzelner Prüfung)") {
EuroTextField(startgebuehr) { startgebuehr = it }
}
ArtikelFormRow("Sporteuro (Beitrag OEPS):", null) {
EuroTextField(sporteuro) { sporteuro = it }
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
ArtikelFormRow("Nachnennungsgebühr:", "(Nach Nennschluss)") {
EuroTextField(nachnennungsgebuehr) { nachnennungsgebuehr = it }
}
ArtikelFormRow("Nennungstausch-Gebühr:", "(Pferd- oder Reiter-Wechsel)") {
EuroTextField(nennungstauschGebuehr) { nennungstauschGebuehr = it }
}
}
}
// ── Stallungen & Boxen ───────────────────────────────────────────────
ArtikelSectionCard(title = "Stallungen & Boxen") {
ArtikelFormRow("Box pro Tag:", null) {
EuroTextField(boxProTag) { boxProTag = it }
}
ArtikelFormRow("Einstreu (Erst-Einstreu):", null) {
EuroTextField(einstreuErstEinstreu) { einstreuErstEinstreu = it }
}
ArtikelFormRow("Einstreu (Nachlegen):", null) {
EuroTextField(einstreuNachlegen) { einstreuNachlegen = it }
}
ArtikelFormRow("Paddock pro Tag:", null) {
EuroTextField(paddockProTag) { paddockProTag = it }
}
}
// ── Zusatzgebühren ───────────────────────────────────────────────────
ArtikelSectionCard(
title = "Zusatzgebühren",
action = {
TextButton(onClick = {
zusatzgebuehren = zusatzgebuehren + ZusatzgebuehrUiModel("", "0.00", false)
}) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp), tint = AccentBlue)
Spacer(Modifier.width(4.dp))
Text("Hinzufügen", color = AccentBlue, fontSize = 13.sp)
}
},
) {
// Tabellen-Header
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
Text("Bezeichnung", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
Text("Betrag", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
Text("Pflicht", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Spacer(Modifier.width(44.dp))
}
HorizontalDivider()
zusatzgebuehren.forEachIndexed { idx, z ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
value = z.bezeichnung,
onValueChange = { v ->
zusatzgebuehren = zusatzgebuehren.toMutableList().also { it[idx] = z.copy(bezeichnung = v) }
},
modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp),
singleLine = true,
)
OutlinedTextField(
value = z.betrag,
onValueChange = { v ->
zusatzgebuehren = zusatzgebuehren.toMutableList().also { it[idx] = z.copy(betrag = v) }
},
suffix = { Text("") },
modifier = Modifier.weight(1.5f).height(44.dp).padding(end = 8.dp),
singleLine = true,
)
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = z.pflicht,
onCheckedChange = { v ->
zusatzgebuehren = zusatzgebuehren.toMutableList().also { it[idx] = z.copy(pflicht = v) }
},
)
Text("Pflicht", fontSize = 12.sp)
}
IconButton(
onClick = { zusatzgebuehren = zusatzgebuehren.toMutableList().also { it.removeAt(idx) } },
modifier = Modifier.size(44.dp),
) {
Icon(
Icons.Default.Delete,
contentDescription = "Löschen",
tint = DeleteRed,
modifier = Modifier.size(18.dp)
)
}
}
}
}
// ── Hinweis ──────────────────────────────────────────────────────────
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFFFFBEB),
shape = MaterialTheme.shapes.small,
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFFDE68A)),
) {
Row(modifier = Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("", fontSize = 14.sp)
Column {
Text("Hinweis zur Preisliste", fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Text(
"Die Gebührenstruktur wird in der offiziellen Ausschreibung veröffentlicht und ist für alle Teilnehmer verbindlich. " +
"Bei nationalen Turnieren der Kategorie C-Neu sind oft reduzierte Gebühren oder Gebührenbefreiungen üblich (z.B. kein Nenngeld, kein Sporteuro).",
fontSize = 12.sp,
color = Color(0xFF92400E),
)
}
}
}
// ── Aktions-Buttons ──────────────────────────────────────────────────
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
OutlinedButton(onClick = {}) { Text("Zurücksetzen") }
Spacer(Modifier.width(8.dp))
Button(onClick = {}, colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)) { Text("Speichern") }
}
}
}
@Composable
private fun ArtikelSectionCard(
title: String,
action: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
action?.invoke()
}
content()
}
}
}
@Composable
private fun ArtikelSubSection(title: String, content: @Composable ColumnScope.() -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9FAFB)),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(title, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
content()
}
}
}
@Composable
private fun ArtikelFormRow(label: String, hint: String?, content: @Composable RowScope.() -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(label, fontSize = 13.sp, modifier = Modifier.width(220.dp), color = Color(0xFF374151))
content()
if (hint != null) {
Spacer(Modifier.width(8.dp))
Text(
hint,
fontSize = 11.sp,
color = Color(0xFF9CA3AF),
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
)
}
}
}
@Composable
private fun EuroTextField(value: String, onValueChange: (String) -> Unit) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
suffix = { Text("") },
modifier = Modifier.width(120.dp).height(44.dp),
singleLine = true,
)
}
// --- UI-Modelle ---
data class ZusatzgebuehrUiModel(val bezeichnung: String, val betrag: String, val pflicht: Boolean)
@@ -0,0 +1,644 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val HeaderBg = Color(0xFFF1F5F9)
private val SelectedRowBg = Color(0xFFEFF6FF)
/**
* BEWERBE-Tab gemäß Vision_03 (Screenshots 0912).
*
* Layout: 3-spaltig
* - Links (140dp): Aktions-Buttons (Speichern, Rückgängig, Einfügen, Löschen, Teilen, Verschieben, Startliste, Ergebnisliste)
* - Mitte (flex): Datentabelle (Tag | Platz | Bewerb | Beginn | Ende | Bewerbname | ZNS | Nennungen)
* - Rechts (340dp): Detail-Panel mit Sub-Tabs (Bewerb | Bewertung | Geldpreise | Ort/Zeit)
*/
@Composable
fun BewerbeTabContent() {
var selectedIndex by remember { mutableIntStateOf(0) }
val bewerbe = remember { sampleBewerbe() }
Row(modifier = Modifier.fillMaxSize()) {
// ── Linke Aktions-Spalte ──────────────────────────────────────────────
BewerbeAktionsSpalte(modifier = Modifier.width(140.dp).fillMaxHeight())
VerticalDivider()
// ── Mittlere Tabelle ──────────────────────────────────────────────────
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
// Toolbar über der Tabelle
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(4.dp))
Text("Aktualisieren", fontSize = 12.sp)
}
Surface(
shape = MaterialTheme.shapes.small,
color = PrimaryBlue,
) {
Text(
text = "${bewerbe.size} Bewerbe",
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
)
}
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
) {
Text("Filtern", fontSize = 12.sp)
}
}
// Tabellen-Header
BewerbeTableHeader()
HorizontalDivider()
// Tabellen-Zeilen
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(bewerbe) { index, bewerb ->
BewerbeTableRow(
bewerb = bewerb,
isSelected = index == selectedIndex,
onClick = { selectedIndex = index },
)
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
}
VerticalDivider()
// ── Rechtes Detail-Panel ──────────────────────────────────────────────
BewerbeDetailPanel(
bewerb = bewerbe.getOrNull(selectedIndex),
modifier = Modifier.width(340.dp).fillMaxHeight(),
)
}
}
@Composable
private fun BewerbeTableHeader() {
Row(
modifier = Modifier
.fillMaxWidth()
.background(HeaderBg)
.padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
TableHeaderCell("Tag", 90.dp)
TableHeaderCell("Platz", 50.dp)
TableHeaderCell("Bewerb", 55.dp)
TableHeaderCell("Beginn", 55.dp)
TableHeaderCell("Ende", 55.dp)
TableHeaderCell("Bewerbname", weight = 1f)
TableHeaderCell("ZNS", 45.dp)
TableHeaderCell("Nennungen", 75.dp)
}
}
@Composable
private fun RowScope.TableHeaderCell(text: String, width: androidx.compose.ui.unit.Dp? = null, weight: Float? = null) {
val mod = when {
weight != null -> Modifier.weight(weight)
width != null -> Modifier.width(width)
else -> Modifier
}
Text(
text = text,
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF374151),
modifier = mod,
)
}
@Composable
private fun BewerbeTableRow(bewerb: BewerbUiModel, isSelected: Boolean, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(if (isSelected) SelectedRowBg else Color.Transparent)
.clickable(onClick = onClick)
.padding(horizontal = 8.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(bewerb.tag, fontSize = 12.sp, modifier = Modifier.width(90.dp))
Text("${bewerb.platz}", fontSize = 12.sp, modifier = Modifier.width(50.dp))
Text(
"${bewerb.nummer}",
fontSize = 12.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
modifier = Modifier.width(55.dp),
color = if (isSelected) PrimaryBlue else Color.Unspecified
)
Text(bewerb.beginn, fontSize = 12.sp, modifier = Modifier.width(55.dp))
Text(bewerb.ende, fontSize = 12.sp, modifier = Modifier.width(55.dp))
Text(bewerb.name, fontSize = 12.sp, modifier = Modifier.weight(1f), maxLines = 2)
Text("${bewerb.zns}", fontSize = 12.sp, modifier = Modifier.width(45.dp))
Text("${bewerb.nennungen}", fontSize = 12.sp, modifier = Modifier.width(75.dp))
}
}
@Composable
private fun BewerbeAktionsSpalte(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
AktionsBtn("Änderungen\nSpeichern")
AktionsBtn("Änderungen\nRückgängig")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
AktionsBtn("Bewerb\nEinfügen")
AktionsBtn("Bewerb\nLöschen")
AktionsBtn("Bewerb Teilen")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
AktionsBtn("Bewerb nach\noben verschieben")
AktionsBtn("Bewerb nach\nunten verschieben")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
AktionsBtn("Startliste\nBearbeiten")
AktionsBtn("Startliste\nDrucken")
AktionsBtn("Ergebnisliste\nBearbeiten")
AktionsBtn("Ergebnisliste\nDrucken")
}
}
@Composable
private fun AktionsBtn(label: String) {
OutlinedButton(
onClick = {},
modifier = Modifier.fillMaxWidth().height(48.dp),
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 2.dp),
) {
Text(label, fontSize = 11.sp, lineHeight = 13.sp)
}
}
@Composable
private fun BewerbeDetailPanel(bewerb: BewerbUiModel?, modifier: Modifier = Modifier) {
var subTab by remember { mutableIntStateOf(0) }
val subTabs = listOf("Bewerb", "Bewertung", "Geldpreise", "Ort/Zeit")
Column(modifier = modifier) {
PrimaryTabRow(
selectedTabIndex = subTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = PrimaryBlue,
) {
subTabs.forEachIndexed { i, title ->
Tab(
selected = subTab == i,
onClick = { subTab = i },
text = { Text(title, fontSize = 12.sp) },
)
}
}
HorizontalDivider()
Box(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
when (subTab) {
0 -> BewerbSubTab(bewerb)
1 -> BewertungSubTab(bewerb)
2 -> GeldpreiseSubTab()
3 -> OrtZeitSubTab(bewerb)
}
}
}
}
// ── Sub-Tab: Bewerb ───────────────────────────────────────────────────────────
@Composable
private fun BewerbSubTab(bewerb: BewerbUiModel?) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
DetailField("Nummer:", bewerb?.nummer?.toString() ?: "")
DetailField("Abteilung:", "")
DetailField("Typ:", bewerb?.typ ?: "")
DetailField("Name:", bewerb?.name ?: "")
DetailField("Bezeichnung:", bewerb?.bezeichnung ?: "")
DetailDropdown("Kategorie:")
DetailDropdown("Klasse:")
DetailDropdown("Lizenz:")
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Maximal:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
OutlinedTextField(
value = "3",
onValueChange = {},
modifier = Modifier.width(60.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
Spacer(Modifier.width(8.dp))
Text("Pferde je Reiter", fontSize = 12.sp, color = Color(0xFF6B7280))
}
DetailDropdown("Pferdealter:")
DetailField("Zeile 1:", bewerb?.zeile1 ?: "")
DetailField("Zeile 2:", "")
DetailField("Zeile 3:", "")
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Logo Bewerb:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.weight(1f),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
Spacer(Modifier.width(4.dp))
OutlinedButton(
onClick = {},
modifier = Modifier.height(40.dp),
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Text("", fontSize = 13.sp)
}
}
}
}
// ── Sub-Tab: Bewertung ────────────────────────────────────────────────────────
@Composable
private fun BewertungSubTab(bewerb: BewerbUiModel?) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Bewertungs-Konfiguration", fontWeight = FontWeight.SemiBold, fontSize = 13.sp, color = Color(0xFF374151))
DetailField("Prüfung:", "Dressurreiterprüfung")
DetailField("Richtverfahren:", "A")
DetailField("Para-Grade:", "")
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Richteranzahl:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
OutlinedTextField(
value = "2",
onValueChange = {},
modifier = Modifier.width(60.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
}
DetailField("Aufgabe:", "Aufgabe R")
DetailField("Aufgabennummer:", "")
DetailField("Maximalpunkte:", "")
HorizontalDivider()
Text("Richter", fontSize = 12.sp, color = Color(0xFF6B7280))
RichterRow("C:", "Schuster Alexandra")
RichterRow("C:", "Vankova Kamila (CZ)")
}
}
@Composable
private fun RichterRow(position: String, name: String) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(position, fontSize = 13.sp, modifier = Modifier.width(30.dp))
OutlinedTextField(
value = name,
onValueChange = {},
modifier = Modifier.weight(1f),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
Checkbox(checked = true, onCheckedChange = {})
}
}
// ── Sub-Tab: Geldpreise ───────────────────────────────────────────────────────
@Composable
private fun GeldpreiseSubTab() {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
// Geldpreis-Sektion
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Geldpreis", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = false, onCheckedChange = {})
Text("Geldpreis", fontSize = 13.sp)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Startgeld:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
OutlinedTextField(
value = "15,00",
onValueChange = {},
modifier = Modifier.width(100.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Auszahlung:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
DetailDropdown("fortführend", modifier = Modifier.weight(1f))
}
}
}
// Geldpreis für Kadererreiter
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Geldpreis für Kadererreiter", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = false, onCheckedChange = {})
Text("Geldpreis für Kadererreiter", fontSize = 13.sp)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Startgeld für Kadererreiter:", fontSize = 13.sp, modifier = Modifier.width(180.dp))
OutlinedTextField(
value = "15,00",
onValueChange = {},
modifier = Modifier.width(100.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
}
}
}
// Geldpreisvorlage
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Geldpreisvorlage wählen:", fontSize = 13.sp, modifier = Modifier.width(180.dp))
DetailDropdown("", modifier = Modifier.weight(1f))
}
// Tabelle
Text("0 Geldpreise", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
Row(
modifier = Modifier.fillMaxWidth().background(HeaderBg).padding(horizontal = 8.dp, vertical = 6.dp),
) {
Text("Nummer", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Geldpreis", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
}
HorizontalDivider()
Text("", fontSize = 12.sp, color = Color(0xFF9CA3AF), modifier = Modifier.padding(8.dp))
}
}
// ── Sub-Tab: Ort/Zeit ─────────────────────────────────────────────────────────
@Composable
private fun OrtZeitSubTab(bewerb: BewerbUiModel?) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Tag:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
DetailDropdown(bewerb?.tag ?: "28.05.2023", modifier = Modifier.weight(1f))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Beginnzeit:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
DetailDropdown("fix um", modifier = Modifier.width(100.dp))
}
LabeledTimeField("Beginnzeit:", bewerb?.beginn ?: "08:00", "(hh:mm)")
LabeledTimeField("Reitdauer:", "02:00", "(mm:ss)")
LabeledTimeField("Umbau:", "10", "(mm)")
LabeledTimeField("Besichtigung:", "10", "(mm)")
LabeledTimeField("Stechen:", "", "(mm)")
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Platz:", fontSize = 13.sp, modifier = Modifier.width(130.dp))
DetailDropdown("Vorderer Turnierplatz", modifier = Modifier.weight(1f))
}
}
}
@Composable
private fun LabeledTimeField(label: String, value: String, unit: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(label, fontSize = 13.sp, modifier = Modifier.width(130.dp))
OutlinedTextField(
value = value,
onValueChange = {},
modifier = Modifier.width(80.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
Spacer(Modifier.width(8.dp))
Text(unit, fontSize = 12.sp, color = Color(0xFF6B7280))
}
}
// ── Hilfs-Composables ─────────────────────────────────────────────────────────
@Composable
private fun DetailField(label: String, value: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(label, fontSize = 13.sp, modifier = Modifier.width(130.dp))
OutlinedTextField(
value = value,
onValueChange = {},
modifier = Modifier.weight(1f),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
)
}
}
@Composable
private fun DetailDropdown(placeholder: String, modifier: Modifier = Modifier) {
OutlinedTextField(
value = placeholder,
onValueChange = {},
modifier = modifier,
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
trailingIcon = {
Text("", fontSize = 10.sp, color = Color(0xFF6B7280))
},
)
}
// ── UI-Modell ─────────────────────────────────────────────────────────────────
data class BewerbUiModel(
val tag: String,
val platz: Int,
val nummer: Int,
val beginn: String,
val ende: String,
val name: String,
val bezeichnung: String,
val typ: String,
val zeile1: String,
val zns: Int,
val nennungen: Int,
)
private fun sampleBewerbe() = listOf(
BewerbUiModel(
"28.05.2023",
1,
1,
"08:00",
"08:00",
"Dressurreiterprüfung Reiterpass\n(Aufgabe R 1)\nPony Einsteiger Cup OO",
"Dressurreiterprüfung Reiterpass",
"Dressur",
"Pony Einsteiger Cup OO",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
2,
"08:20",
"08:20",
"Dressurreiterprüfung Reitenadel\n(Aufgabe R 4)\nPony Einsteiger Cup OO",
"Dressurreiterprüfung Reitenadel",
"Dressur",
"Pony Einsteiger Cup OO",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
3,
"08:40",
"08:40",
"Dressurreiterprüfung lsf. (Istzfrei)\n(Aufgabe LF 1)",
"Dressurreiterprüfung lsf.",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
4,
"09:00",
"09:00",
"Dressurreiterprüfung lsf. (Lizenzfrei)\n(Aufgabe LF 3)",
"Dressurreiterprüfung lsf.",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
5,
"09:20",
"09:20",
"Führzügelklasse\nOO Kids Cup",
"Führzügelklasse",
"Dressur",
"OO Kids Cup",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
6,
"09:40",
"09:40",
"First Ridden\nOO Kids Cup",
"First Ridden",
"Dressur",
"OO Kids Cup",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
7,
"10:00",
"10:00",
"Pony Dressurprüfung Kl. A (Aufgabe P 1)",
"Pony Dressurprüfung Kl. A",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
8,
"10:20",
"10:20",
"Dressurreiterprüfung Kl. A (Aufgabe DRA 1)",
"Dressurreiterprüfung Kl. A",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
9,
"10:40",
"10:40",
"Dressurreiterprüfung Kl. A (Aufgabe A 5)",
"Dressurreiterprüfung Kl. A",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
10,
"11:00",
"11:00",
"Pony Dressurprüfung Kl. A (Aufgabe P 9)",
"Pony Dressurprüfung Kl. A",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
11,
"11:20",
"11:20",
"Dressurreiterprüfung Kl. L (Aufgabe DRL 1)",
"Dressurreiterprüfung Kl. L",
"Dressur",
"",
0,
0
),
BewerbUiModel(
"28.05.2023",
1,
12,
"11:40",
"11:40",
"Dressurprüfung Kl. L (Aufgabe L 3)",
"Dressurprüfung Kl. L",
"Dressur",
"",
0,
0
),
)
@@ -0,0 +1,93 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* Detailansicht eines Turniers gemäß Vision_03.
*
* Layout: Horizontale Tab-Bar mit 8 Tabs (kein eigener Toolbar-Zurück-Button
* Navigation erfolgt über den Breadcrumb in der TopBar).
*
* Tabs:
* 1. STAMMDATEN Turnier-Konfiguration, ZNS-Import, Sparten, Datum
* 2. ORGANISATION Funktionäre, Richterkollegium, Austragungsplätze
* 3. BEWERBE 3-spaltiges Layout (Aktionen | Tabelle | Detail-Panel)
* 4. ARTIKEL Gebühren, Stallungen & Boxen, Zusatzgebühren
* 5. ABRECHNUNG Buchungen, Offene Posten, Rechnung
* 6. NENNUNGEN Pferd+Reiter-Suche, Verkauf/Buchungen, Bewerbsübersicht
* 7. STARTLISTEN Bewerbs-Tabs, Sortierung, Zeit/Dauer
* 8. ERGEBNISLISTEN Bewerbs-Tabs, Platzierung & Geldpreise
*
*/
@Composable
fun TurnierDetailScreen(
veranstaltungId: Long,
turnierId: Long,
onBack: () -> Unit,
) {
var selectedTab by remember { mutableIntStateOf(0) }
val tabs = listOf(
"STAMMDATEN",
"ORGANISATION",
"BEWERBE",
"ARTIKEL",
"ABRECHNUNG",
"NENNUNGEN",
"STARTLISTEN",
"ERGEBNISLISTEN",
)
Column(modifier = Modifier.fillMaxSize()) {
// Horizontale Tab-Bar (direkt unter der TopBar)
PrimaryScrollableTabRow(
selectedTabIndex = selectedTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = Color(0xFF1E3A8A),
edgePadding = 0.dp,
) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = {
Text(
text = title,
fontSize = 13.sp,
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
)
},
)
}
}
HorizontalDivider()
// Tab-Inhalte
Box(modifier = Modifier.fillMaxSize()) {
when (selectedTab) {
0 -> StammdatenTabContent(turnierId = turnierId)
1 -> OrganisationTabContent()
2 -> BewerbeTabContent()
3 -> ArtikelTabContent()
4 -> AbrechnungTabContent()
5 -> NennungenTabContent()
6 -> StartlistenTabContent()
7 -> ErgebnislistenTabContent()
}
}
}
}
// Tab-Inhalte werden in dedizierten Dateien implementiert:
// TurnierBewerbeTab.kt → BewerbeTabContent()
// TurnierNennungenTab.kt → NennungenTabContent()
// TurnierStartlistenTab.kt → StartlistenTabContent()
// TurnierErgebnislistenTab.kt → ErgebnislistenTabContent()
@@ -0,0 +1,215 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val ElBlue = Color(0xFF1E3A8A)
private val ElHeaderBg = Color(0xFFF1F5F9)
/**
* ERGEBNISLISTEN-Tab gemäß Vision_03.
*
* Layout: 2-spaltig
* - Links (flex): Bewerbs-Tabs + Ergebnis-Tabelle (Platz | Startnr | Pferd | Reiter | Fehler | Zeit | Punkte)
* - Rechts (280dp): Platzierung & Geldpreis-Panel
*/
@Composable
fun ErgebnislistenTabContent() {
Row(modifier = Modifier.fillMaxSize()) {
// ── Linke Spalte: Bewerbs-Tabs + Tabelle ─────────────────────────────
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
ErgebnislistenBewerbsTabs()
}
VerticalDivider()
// ── Rechte Spalte: Platzierung & Geldpreis ───────────────────────────
PlatzierungGeldpreisPanel(modifier = Modifier.width(280.dp).fillMaxHeight())
}
}
@Composable
private fun ErgebnislistenBewerbsTabs() {
val bewerbe = remember {
listOf("Bewerb 1", "Bewerb 2", "Bewerb 3", "Bewerb 4", "Bewerb 5")
}
var selectedBewerb by remember { mutableIntStateOf(0) }
PrimaryScrollableTabRow(
selectedTabIndex = selectedBewerb,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = ElBlue,
edgePadding = 0.dp,
) {
bewerbe.forEachIndexed { index, title ->
Tab(
selected = selectedBewerb == index,
onClick = { selectedBewerb = index },
text = { Text(title, fontSize = 12.sp) },
)
}
}
HorizontalDivider()
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Bewerb ${selectedBewerb + 1} Ergebnisliste",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
)
Spacer(Modifier.weight(1f))
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
Text("Importieren", fontSize = 12.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
Text("Exportieren", fontSize = 12.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
Text("Drucken", fontSize = 12.sp)
}
}
// Tabellen-Header
Row(
modifier = Modifier.fillMaxWidth().background(ElHeaderBg).padding(horizontal = 12.dp, vertical = 6.dp),
) {
Text("Platz", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(50.dp))
Text("Startnr.", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(65.dp))
Text("Pferd", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Reiter", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Fehler", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
Text("Zeit", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
Text("Punkte", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
}
HorizontalDivider()
// Leere Liste
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Keine Ergebnisse vorhanden", fontSize = 14.sp, color = Color(0xFF6B7280))
Spacer(Modifier.height(8.dp))
Text("Ergebnisse werden nach dem Turnier eingetragen.", fontSize = 12.sp, color = Color(0xFF9CA3AF))
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = {}) {
Text("Ergebnisse importieren", fontSize = 13.sp)
}
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = ElBlue),
) {
Text("Ergebnisse eingeben", fontSize = 13.sp)
}
}
}
}
}
@Composable
private fun PlatzierungGeldpreisPanel(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Platzierung & Geldpreis", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
HorizontalDivider()
// Anzahl Platzierte
Text("Platzierung", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Anzahl Platzierte:", fontSize = 12.sp, modifier = Modifier.width(140.dp))
OutlinedTextField(
value = "3",
onValueChange = {},
modifier = Modifier.width(60.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Stechen ab Platz:", fontSize = 12.sp, modifier = Modifier.width(140.dp))
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.width(60.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
)
}
HorizontalDivider()
// Geldpreise
Text("Geldpreise", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
Row(
modifier = Modifier.fillMaxWidth().background(ElHeaderBg).padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text("Platz", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(50.dp))
Text("Betrag (€)", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
}
HorizontalDivider()
listOf(1 to "", 2 to "", 3 to "").forEach { (platz, betrag) ->
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
"$platz.",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = ElBlue,
modifier = Modifier.width(50.dp)
)
OutlinedTextField(
value = betrag,
onValueChange = {},
modifier = Modifier.weight(1f).height(36.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
)
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
HorizontalDivider()
// Aktions-Buttons
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = ElBlue),
modifier = Modifier.fillMaxWidth(),
) {
Text("Platzierung berechnen", fontSize = 12.sp)
}
OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth()) {
Text("Ergebnisliste drucken", fontSize = 12.sp)
}
OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth()) {
Text("Geldpreise auszahlen", fontSize = 12.sp)
}
}
}
}
@@ -0,0 +1,251 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val NennBlue = Color(0xFF1E3A8A)
private val NennHeaderBg = Color(0xFFF1F5F9)
private val NennSelectedBg = Color(0xFFEFF6FF)
/**
* NENNUNGEN-Tab gemäß Vision_03.
*
* Layout: 2-spaltig
* - Links (flex): Pferd+Reiter-Suche + Nennungs-Tabelle
* - Rechts (360dp): Verkauf/Buchungen + Bewerbsübersicht
*/
@Composable
fun NennungenTabContent() {
Row(modifier = Modifier.fillMaxSize()) {
// ── Linke Spalte: Suche + Tabelle ─────────────────────────────────────
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
NennungenSuchePanel()
HorizontalDivider()
NennungenTabelle()
}
VerticalDivider()
// ── Rechte Spalte: Verkauf + Bewerbsübersicht ─────────────────────────
Column(
modifier = Modifier
.width(360.dp)
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) {
VerkaufBuchungenPanel()
HorizontalDivider()
BewerbsuebersichtPanel()
}
}
}
@Composable
private fun NennungenSuchePanel() {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Pferd & Reiter suchen", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = "",
onValueChange = {},
placeholder = { Text("Pferd suchen (Name, OEPS-Nr.)…", fontSize = 12.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(16.dp)) },
modifier = Modifier.weight(1f).height(44.dp),
singleLine = true,
)
OutlinedTextField(
value = "",
onValueChange = {},
placeholder = { Text("Reiter suchen (Name, OEPS-Nr.)…", fontSize = 12.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(16.dp)) },
modifier = Modifier.weight(1f).height(44.dp),
singleLine = true,
)
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = NennBlue),
modifier = Modifier.height(44.dp),
) {
Text("Suchen", fontSize = 12.sp)
}
}
}
}
@Composable
private fun NennungenTabelle() {
val nennungen = remember { sampleNennungen() }
var selectedIndex by remember { mutableIntStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
// Header
Row(
modifier = Modifier
.fillMaxWidth()
.background(NennHeaderBg)
.padding(horizontal = 12.dp, vertical = 6.dp),
) {
Text("Startnr.", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
Text("Pferd", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Reiter", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Bewerb", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Status", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(80.dp))
}
HorizontalDivider()
if (nennungen.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Keine Nennungen vorhanden", fontSize = 14.sp, color = Color(0xFF6B7280))
Spacer(Modifier.height(8.dp))
Text(
"Suchen Sie nach Pferd und Reiter, um eine Nennung hinzuzufügen.",
fontSize = 12.sp,
color = Color(0xFF9CA3AF)
)
}
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(nennungen) { index, nennung ->
Row(
modifier = Modifier
.fillMaxWidth()
.background(if (index == selectedIndex) NennSelectedBg else Color.Transparent)
.clickable { selectedIndex = index }
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
"${nennung.startnr}",
fontSize = 12.sp,
modifier = Modifier.width(60.dp),
color = NennBlue,
fontWeight = FontWeight.Bold
)
Text(nennung.pferd, fontSize = 12.sp, modifier = Modifier.weight(1f))
Text(nennung.reiter, fontSize = 12.sp, modifier = Modifier.weight(1f))
Text(nennung.bewerb, fontSize = 12.sp, modifier = Modifier.weight(1f))
NennungStatusBadge(nennung.status)
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
}
}
}
@Composable
private fun NennungStatusBadge(status: String) {
val (bg, fg) = when (status) {
"Gemeldet" -> Color(0xFFDCFCE7) to Color(0xFF16A34A)
"Bezahlt" -> Color(0xFFDBEAFE) to NennBlue
"Abgemeldet" -> Color(0xFFFEE2E2) to Color(0xFFDC2626)
else -> Color(0xFFF3F4F6) to Color(0xFF6B7280)
}
Surface(shape = MaterialTheme.shapes.small, color = bg) {
Text(
text = status,
fontSize = 10.sp,
color = fg,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
)
}
}
@Composable
private fun VerkaufBuchungenPanel() {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Verkauf / Buchungen", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
// Artikel-Buchungen
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Artikel-Buchungen", fontSize = 12.sp, fontWeight = FontWeight.Medium, color = Color(0xFF374151))
Row(
modifier = Modifier.fillMaxWidth().background(NennHeaderBg).padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text("Artikel", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Menge", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(50.dp))
Text("Preis", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(60.dp))
}
HorizontalDivider()
Text(
"Keine Buchungen",
fontSize = 12.sp,
color = Color(0xFF9CA3AF),
modifier = Modifier.padding(vertical = 8.dp)
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
Text("+ Artikel buchen", fontSize = 11.sp)
}
}
}
}
}
}
@Composable
private fun BewerbsuebersichtPanel() {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Bewerbsübersicht", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
Card(elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(
modifier = Modifier.fillMaxWidth().background(NennHeaderBg).padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text("Bewerb", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Nennungen", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(80.dp))
}
HorizontalDivider()
listOf(
"Bewerb 1 Dressur Kl. A" to 0,
"Bewerb 2 Dressur Kl. L" to 0,
"Bewerb 3 Springen Kl. A" to 0,
).forEach { (name, count) ->
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(name, fontSize = 12.sp, modifier = Modifier.weight(1f))
Text("$count", fontSize = 12.sp, modifier = Modifier.width(80.dp), color = Color(0xFF6B7280))
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
}
}
}
// ── UI-Modell ─────────────────────────────────────────────────────────────────
private data class NennungUiModel(
val startnr: Int,
val pferd: String,
val reiter: String,
val bewerb: String,
val status: String,
)
private fun sampleNennungen(): List<NennungUiModel> = emptyList()
@@ -0,0 +1,65 @@
package at.mocode.turnier.feature.presentation
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.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
/**
* Formular zum Anlegen eines neuen Turniers (Vision_03: /veranstaltung/{id}/turnier/neu).
* Tabs: Übersicht | Stammdaten (A-Satz) | Organisation | Bewerbe ⭐ | Preisliste
* TODO: Echte Formular-Felder und Persistenz (Phase 4/5).
*/
@Composable
fun TurnierNeuScreen(
veranstaltungId: Long,
onBack: () -> Unit,
onSave: () -> Unit,
) {
var selectedTab by remember { mutableIntStateOf(3) } // Bewerbe ist Standard-Tab (⭐)
val tabs = listOf("Übersicht", "Stammdaten (A-Satz)", "Organisation", "Bewerbe ⭐", "Preisliste")
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Spacer(Modifier.width(8.dp))
Text(
text = "Neues Turnier (Veranstaltung #$veranstaltungId)",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.alignByBaseline(),
)
}
Button(onClick = onSave) { Text("Speichern") }
}
PrimaryTabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) },
)
}
}
Box(modifier = Modifier.fillMaxSize().padding(24.dp)) {
when (selectedTab) {
0 -> PlaceholderContent("Übersicht", "Wird nach dem Speichern befüllt.")
1 -> PlaceholderContent("Stammdaten (A-Satz)", "OEPS-Turniernummer, Kategorie, Sparte …")
2 -> PlaceholderContent("Organisation", "Richter, Parcourschef, Tierarzt …")
3 -> PlaceholderContent("Bewerbe", "Bewerbe anlegen und Abteilungen konfigurieren …")
4 -> PlaceholderContent("Preisliste", "Nenngebühren pro Bewerb/Sparte …")
}
}
}
}
@@ -0,0 +1,336 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
private val DeleteRed = Color(0xFFDC2626)
/**
* ORGANISATION-Tab im TurnierDetailScreen.
* Gemäß Figma Vision_03 (figma-entwurf_13 / figma-entwurf_14):
* - Funktionäre & Offizielle (C-Satz): Turnierleiter, Turnierbeauftragter, Technischer Delegierter, Parcourschef
* - Support-Team: Tierarzt, Schmied, Steward
* - Richterkollegium: dynamische Liste (Name, Qualifikation, Funktion, Löschen)
* - Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen)
*/
@Composable
fun OrganisationTabContent() {
var turnierleiter by remember { mutableStateOf("") }
var turnierbeauftragter by remember { mutableStateOf("") }
var technischerDelegierter by remember { mutableStateOf("") }
var parcourschef by remember { mutableStateOf("") }
var tierarzt by remember { mutableStateOf("") }
var schmied by remember { mutableStateOf("") }
var steward by remember { mutableStateOf("") }
var richter by remember {
mutableStateOf(
listOf(
RichterUiModel("Alexandra Schuster", "D-GP", "Hauptrichter"),
RichterUiModel("Ulrike Knasmüller-Prinz", "D-M", "Beisitzer"),
),
)
}
var plaetze by remember {
mutableStateOf(
listOf(
AustragungsplatzUiModel("Dressur", "20 x 60 m", "Hauptplatz"),
AustragungsplatzUiModel("Dressur", "20 x 40 m", "Abreiteplatz 1"),
),
)
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// ── Funktionäre & Offizielle ─────────────────────────────────────────
OrgSectionCard(title = "Funktionäre & Offizielle (C-Satz)") {
OrgSubSection("Turnier-Organisation") {
OrgSearchField("Turnierleiter:", turnierleiter) { turnierleiter = it }
OrgSearchField("Turnierbeauftragter:", turnierbeauftragter) { turnierbeauftragter = it }
OrgSearchField("Technischer Delegierter:", technischerDelegierter) { technischerDelegierter = it }
OrgSearchField("Parcourschef:", parcourschef) { parcourschef = it }
}
OrgSubSection("Support-Team") {
OrgSearchField("Tierarzt:", tierarzt) { tierarzt = it }
OrgSearchField("Schmied:", schmied) { schmied = it }
OrgSearchField("Steward:", steward) { steward = it }
}
}
// ── Richterkollegium ─────────────────────────────────────────────────
OrgSectionCard(
title = "Richterkollegium",
action = {
TextButton(onClick = {
richter = richter + RichterUiModel("", "", "")
}) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp), tint = AccentBlue)
Spacer(Modifier.width(4.dp))
Text("Richter hinzufügen", color = AccentBlue, fontSize = 13.sp)
}
},
) {
// Tabellen-Header
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
Text("Name", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
Text("Qualifikation", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
Text("Funktion", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
Text("Aktion", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(48.dp))
}
HorizontalDivider()
richter.forEachIndexed { idx, r ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
value = r.name,
onValueChange = { v -> richter = richter.toMutableList().also { it[idx] = r.copy(name = v) } },
modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp),
singleLine = true,
)
// Qualifikation-Dropdown
var qualExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
OutlinedTextField(
value = r.qualifikation,
onValueChange = {},
readOnly = true,
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
modifier = Modifier.fillMaxWidth().height(44.dp),
singleLine = true,
)
DropdownMenu(expanded = qualExpanded, onDismissRequest = { qualExpanded = false }) {
listOf("D-GP", "D-M", "D-L", "S-GP", "S-M").forEach { q ->
DropdownMenuItem(text = { Text(q) }, onClick = {
richter = richter.toMutableList().also { it[idx] = r.copy(qualifikation = q) }
qualExpanded = false
})
}
}
}
// Funktion-Dropdown
var funExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
OutlinedTextField(
value = r.funktion,
onValueChange = {},
readOnly = true,
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
modifier = Modifier.fillMaxWidth().height(44.dp),
singleLine = true,
)
DropdownMenu(expanded = funExpanded, onDismissRequest = { funExpanded = false }) {
listOf("Hauptrichter", "Beisitzer", "Schreiber").forEach { f ->
DropdownMenuItem(text = { Text(f) }, onClick = {
richter = richter.toMutableList().also { it[idx] = r.copy(funktion = f) }
funExpanded = false
})
}
}
}
IconButton(
onClick = { richter = richter.toMutableList().also { it.removeAt(idx) } },
modifier = Modifier.size(44.dp),
) {
Icon(
Icons.Default.Delete,
contentDescription = "Löschen",
tint = DeleteRed,
modifier = Modifier.size(18.dp)
)
}
}
}
}
// ── Austragungsplätze ────────────────────────────────────────────────
OrgSectionCard(
title = "Austragungsplätze",
) {
OrgSubSection(
title = "Plätze & Anlagen",
action = {
TextButton(onClick = {
plaetze = plaetze + AustragungsplatzUiModel("", "", "")
}) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp), tint = AccentBlue)
Spacer(Modifier.width(4.dp))
Text("Platz hinzufügen", color = AccentBlue, fontSize = 13.sp)
}
},
) {
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
Text("Sparte", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
Text("Größe", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.5f))
Text("Bezeichnung", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(3f))
Text("Aktion", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(48.dp))
}
HorizontalDivider()
plaetze.forEachIndexed { idx, p ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var sparteExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
OutlinedTextField(
value = p.sparte,
onValueChange = {},
readOnly = true,
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
modifier = Modifier.fillMaxWidth().height(44.dp),
singleLine = true,
)
DropdownMenu(expanded = sparteExpanded, onDismissRequest = { sparteExpanded = false }) {
listOf("Dressur", "Springen", "Vielseitigkeit").forEach { s ->
DropdownMenuItem(text = { Text(s) }, onClick = {
plaetze = plaetze.toMutableList().also { it[idx] = p.copy(sparte = s) }
sparteExpanded = false
})
}
}
}
var groesseExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {
OutlinedTextField(
value = p.groesse,
onValueChange = {},
readOnly = true,
trailingIcon = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
modifier = Modifier.fillMaxWidth().height(44.dp),
singleLine = true,
)
DropdownMenu(expanded = groesseExpanded, onDismissRequest = { groesseExpanded = false }) {
listOf("20 x 60 m", "20 x 40 m", "60 x 80 m").forEach { g ->
DropdownMenuItem(text = { Text(g) }, onClick = {
plaetze = plaetze.toMutableList().also { it[idx] = p.copy(groesse = g) }
groesseExpanded = false
})
}
}
}
OutlinedTextField(
value = p.bezeichnung,
onValueChange = { v -> plaetze = plaetze.toMutableList().also { it[idx] = p.copy(bezeichnung = v) } },
modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp),
singleLine = true,
)
IconButton(
onClick = { plaetze = plaetze.toMutableList().also { it.removeAt(idx) } },
modifier = Modifier.size(44.dp),
) {
Icon(
Icons.Default.Delete,
contentDescription = "Löschen",
tint = DeleteRed,
modifier = Modifier.size(18.dp)
)
}
}
}
}
}
// ── Speichern ────────────────────────────────────────────────────────
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) { Text("Speichern") }
}
}
}
@Composable
private fun OrgSectionCard(
title: String,
action: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
action?.invoke()
}
content()
}
}
}
@Composable
private fun OrgSubSection(
title: String,
action: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9FAFB)),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(title, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
action?.invoke()
}
content()
}
}
}
@Composable
private fun OrgSearchField(label: String, value: String, onValueChange: (String) -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, fontSize = 13.sp, modifier = Modifier.width(200.dp), color = Color(0xFF374151))
OutlinedTextField(
value = value,
onValueChange = onValueChange,
placeholder = { Text("Name suchen...", fontSize = 12.sp) },
modifier = Modifier.weight(1f).height(44.dp),
singleLine = true,
)
}
}
// --- UI-Modelle ---
data class RichterUiModel(val name: String, val qualifikation: String, val funktion: String)
data class AustragungsplatzUiModel(val sparte: String, val groesse: String, val bezeichnung: String)
@@ -0,0 +1,261 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Usb
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
/**
* STAMMDATEN-Tab im TurnierDetailScreen.
* Gemäß Figma Vision_03 (figma-entwurf_16 / figma-entwurf_15):
* - Turnier-Konfiguration: Nr., Typ (OTO/FEI), ZNS-Import, Sprache
* - Sparten-Checkboxen, Klassen, Kategorien, Datum
* - Turnier-Beschreibung: Titel, Sub-Titel
* - Sponsoren
*/
@Composable
fun StammdatenTabContent(turnierId: Long) {
var turnierNr by remember { mutableStateOf("") }
var typOto by remember { mutableStateOf(true) }
var spracheDe by remember { mutableStateOf(true) }
var sparteDressur by remember { mutableStateOf(false) }
var sparteSpringen by remember { mutableStateOf(false) }
var klasseC by remember { mutableStateOf(false) }
var klasseB by remember { mutableStateOf(false) }
var klasseA by remember { mutableStateOf(false) }
var datumVon by remember { mutableStateOf("") }
var datumBis by remember { mutableStateOf("") }
var titel by remember { mutableStateOf("") }
var subTitel by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// ── Turnier-Konfiguration ────────────────────────────────────────────
SectionCard(title = "Turnier-Konfiguration") {
FormRow("Turnier-Nr.:") {
OutlinedTextField(
value = turnierNr,
onValueChange = { turnierNr = it },
placeholder = { Text("z.B. 26128", fontSize = 13.sp) },
modifier = Modifier.width(200.dp).height(48.dp),
singleLine = true,
)
}
FormRow("Typ:") {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = typOto, onClick = { typOto = true })
Text("OTO (National)", fontSize = 13.sp)
RadioButton(selected = !typOto, onClick = { typOto = false })
Text("FEI (International)", fontSize = 13.sp)
}
}
FormRow("ZNS-Daten:") {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue),
) {
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Import via Internet", fontSize = 13.sp)
}
OutlinedButton(onClick = {}) {
Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Import via USB", fontSize = 13.sp)
}
}
Text(
"Reiter-, Pferde-, Funktionärs- und Vereinsdaten vom OEPS Backend",
fontSize = 11.sp,
color = Color(0xFF6B7280),
)
}
FormRow("Sprache:") {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = spracheDe, onClick = { spracheDe = true })
Text("Deutsch", fontSize = 13.sp)
RadioButton(selected = !spracheDe, onClick = { spracheDe = false })
Text("English", fontSize = 13.sp)
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
FormRow("Sparten:") {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = sparteDressur, onCheckedChange = { sparteDressur = it })
Text("Dressur", fontSize = 13.sp)
Checkbox(checked = sparteSpringen, onCheckedChange = { sparteSpringen = it })
Text("Springen", fontSize = 13.sp)
}
}
FormRow("Klassen:") {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = klasseC, onCheckedChange = { klasseC = it })
Text("C", fontSize = 13.sp)
Checkbox(checked = klasseB, onCheckedChange = { klasseB = it })
Text("B", fontSize = 13.sp)
Checkbox(checked = klasseA, onCheckedChange = { klasseA = it })
Text("A", fontSize = 13.sp)
}
}
FormRow("Kategorien:") {
Surface(
modifier = Modifier.fillMaxWidth().height(60.dp),
color = Color(0xFFF3F4F6),
shape = MaterialTheme.shapes.small,
) {
Box(contentAlignment = Alignment.Center) {
Text(
"Bitte Sparte(n) auswählen",
fontSize = 13.sp,
color = Color(0xFF9CA3AF),
)
}
}
}
FormRow("Datum:") {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = datumVon,
onValueChange = { datumVon = it },
placeholder = { Text("DD.MM.YYYY", fontSize = 12.sp) },
modifier = Modifier.width(160.dp).height(48.dp),
singleLine = true,
)
Text("bis", fontSize = 13.sp)
OutlinedTextField(
value = datumBis,
onValueChange = { datumBis = it },
placeholder = { Text("DD.MM.YYYY", fontSize = 12.sp) },
modifier = Modifier.width(160.dp).height(48.dp),
singleLine = true,
)
}
}
}
// ── Turnier-Beschreibung ─────────────────────────────────────────────
SectionCard(title = "Turnier-Beschreibung") {
OutlinedTextField(
value = titel,
onValueChange = { titel = it },
placeholder = { Text("z.B. Frühjahrs-Turnier 2026", fontSize = 13.sp) },
label = { Text("Titel") },
modifier = Modifier.fillMaxWidth().height(56.dp),
singleLine = true,
)
OutlinedTextField(
value = subTitel,
onValueChange = { subTitel = it },
placeholder = { Text("z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ", fontSize = 13.sp) },
label = { Text("Sub-Titel") },
modifier = Modifier.fillMaxWidth().height(56.dp),
singleLine = true,
)
}
// ── Sponsoren ────────────────────────────────────────────────────────
SectionCard(
title = "Sponsoren",
action = {
TextButton(onClick = {}) {
Text("+ Sponsor hinzufügen", color = AccentBlue, fontSize = 13.sp)
}
},
) {
Surface(
modifier = Modifier.fillMaxWidth().height(80.dp),
color = Color(0xFFF9FAFB),
shape = MaterialTheme.shapes.small,
) {
Box(contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Noch keine Sponsoren hinzugefügt", fontSize = 13.sp, color = Color(0xFF6B7280))
Spacer(Modifier.height(4.dp))
TextButton(onClick = {}) {
Text("+ Ersten Sponsor hinzufügen", color = AccentBlue, fontSize = 13.sp)
}
}
}
}
}
// ── Aktions-Buttons ──────────────────────────────────────────────────
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(onClick = {}) { Text("Zurücksetzen") }
Spacer(Modifier.width(8.dp))
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) { Text("Speichern") }
}
}
}
@Composable
private fun SectionCard(
title: String,
action: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(title, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
action?.invoke()
}
content()
}
}
}
@Composable
private fun FormRow(label: String, content: @Composable ColumnScope.() -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
) {
Text(
text = label,
fontSize = 13.sp,
modifier = Modifier.width(140.dp).padding(top = 12.dp),
color = Color(0xFF374151),
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
content()
}
}
}
@@ -0,0 +1,182 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val SlBlue = Color(0xFF1E3A8A)
private val SlHeaderBg = Color(0xFFF1F5F9)
/**
* STARTLISTEN-Tab gemäß Vision_03.
*
* Layout: 2-spaltig
* - Links (flex): Bewerbs-Tabs + Starter-Tabelle (Startnr | Pferd | Reiter | Abteilung | Beginn)
* - Rechts (280dp): Sortierung & Zeit-Panel
*/
@Composable
fun StartlistenTabContent() {
Row(modifier = Modifier.fillMaxSize()) {
// ── Linke Spalte: Bewerbs-Tabs + Tabelle ─────────────────────────────
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
StartlistenBewerbsTabs()
}
VerticalDivider()
// ── Rechte Spalte: Sortierung & Zeit ─────────────────────────────────
StartlistenSortierPanel(modifier = Modifier.width(280.dp).fillMaxHeight())
}
}
@Composable
private fun StartlistenBewerbsTabs() {
val bewerbe = remember {
listOf("Bewerb 1", "Bewerb 2", "Bewerb 3", "Bewerb 4", "Bewerb 5")
}
var selectedBewerb by remember { mutableIntStateOf(0) }
PrimaryScrollableTabRow(
selectedTabIndex = selectedBewerb,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = SlBlue,
edgePadding = 0.dp,
) {
bewerbe.forEachIndexed { index, title ->
Tab(
selected = selectedBewerb == index,
onClick = { selectedBewerb = index },
text = { Text(title, fontSize = 12.sp) },
)
}
}
HorizontalDivider()
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Bewerb ${selectedBewerb + 1} Startliste",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
)
Spacer(Modifier.weight(1f))
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
Text("Drucken", fontSize = 12.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.height(32.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
Text("Exportieren", fontSize = 12.sp)
}
}
// Tabellen-Header
Row(
modifier = Modifier.fillMaxWidth().background(SlHeaderBg).padding(horizontal = 12.dp, vertical = 6.dp),
) {
Text("Startnr.", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
Text("Pferd", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Reiter", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
Text("Abteilung", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(80.dp))
Text("Beginn", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(70.dp))
}
HorizontalDivider()
// Leere Liste
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Keine Starter vorhanden", fontSize = 14.sp, color = Color(0xFF6B7280))
Spacer(Modifier.height(8.dp))
Text("Startliste wird nach Nennungsschluss generiert.", fontSize = 12.sp, color = Color(0xFF9CA3AF))
Spacer(Modifier.height(16.dp))
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = SlBlue),
) {
Text("Startliste generieren", fontSize = 13.sp)
}
}
}
}
@Composable
private fun StartlistenSortierPanel(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Sortierung & Zeit", fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
HorizontalDivider()
// Sortierung
Text("Sortierung", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
SortierOption("Aufsteigend (Startnummer)")
SortierOption("Absteigend (Startnummer)")
SortierOption("Auslosung (zufällig)")
SortierOption("Alphabetisch (Pferd)")
SortierOption("Alphabetisch (Reiter)")
}
HorizontalDivider()
// Zeiten
Text("Zeiten", fontSize = 12.sp, color = Color(0xFF374151), fontWeight = FontWeight.Medium)
LabeledInput("Beginnzeit:", "08:00", "(hh:mm)")
LabeledInput("Reitdauer:", "02:00", "(mm:ss)")
LabeledInput("Umbau:", "10", "(mm)")
LabeledInput("Besichtigung:", "10", "(mm)")
HorizontalDivider()
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = SlBlue),
modifier = Modifier.fillMaxWidth(),
) {
Text("Zeiten neu berechnen", fontSize = 12.sp)
}
}
}
@Composable
private fun SortierOption(label: String) {
var selected by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
) {
RadioButton(selected = selected, onClick = { selected = !selected })
Text(label, fontSize = 12.sp)
}
}
@Composable
private fun LabeledInput(label: String, value: String, unit: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(label, fontSize = 12.sp, modifier = Modifier.width(100.dp))
OutlinedTextField(
value = value,
onValueChange = {},
modifier = Modifier.width(70.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
)
Spacer(Modifier.width(6.dp))
Text(unit, fontSize = 11.sp, color = Color(0xFF6B7280))
}
}