chore: entferne nicht genutzte NennungsMaske-Komponente, extrahiere AktionsButtonLeiste in separaten Komponentenordner

This commit is contained in:
2026-04-19 00:52:12 +02:00
parent 1b20e480f4
commit 64d749be3a
31 changed files with 2704 additions and 2970 deletions
@@ -40,6 +40,9 @@ data class Bewerb(
// --- Startwunsch ---
enum class Startwunsch { VORNE, HINTEN, KEINE_PRAEFERENZ }
enum class NennungTab { REITER, PFERD, BEWERBE }
enum class VerkaufTab { VERKAUF, BUCHUNGEN }
// --- Nennung ---
data class Nennung(
val tag: String,
@@ -0,0 +1,162 @@
package at.mocode.frontend.features.nennung.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.unit.dp
import at.mocode.frontend.features.nennung.domain.*
import at.mocode.frontend.features.nennung.presentation.components.*
import at.mocode.frontend.features.nennung.presentation.online.OnlineNennungEingang
import at.mocode.frontend.features.nennung.presentation.tabs.*
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun NennungManagementScreen(
viewModel: NennungViewModel,
onStartlisteOeffnen: () -> Unit = {},
onErgebnisseOeffnen: () -> Unit = {},
onAbrechnungOeffnen: () -> Unit = {},
) {
val state by viewModel.uiState.collectAsState()
// Status-Snackbar
state.statusMeldung?.let { meldung ->
LaunchedEffect(meldung) {
kotlinx.coroutines.delay(3000.milliseconds)
viewModel.statusMeldungDismiss()
}
}
Column(modifier = Modifier.fillMaxSize()) {
// --- Status-Banner ---
state.statusMeldung?.let { meldung ->
Surface(
color = if (meldung.startsWith("")) Color(0xFF388E3C)
else if (meldung.startsWith("⚠️")) Color(0xFFF57C00)
else MaterialTheme.colorScheme.error,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = meldung,
color = Color.White,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelSmall,
)
}
}
// --- Zeile 1: Online-Nennungen (nur wenn vorhanden, 20% Höhe) ---
if (state.onlineNennungen.isNotEmpty()) {
Box(modifier = Modifier.fillMaxWidth().height(150.dp)) {
OnlineNennungEingang(
state = state,
onRefresh = viewModel::loadOnlineNennungen,
onUebernehmen = viewModel::uebernehmeOnlineNennung
)
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
}
// --- Zeile 2: Pferd/Reiter + Verkauf/Buchungen (40% Höhe) ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.4f)
) {
// Linke Hälfte: Pferd & Reiter Suche (60%)
Column(
modifier = Modifier
.weight(0.6f)
.fillMaxHeight()
) {
PferdReiterEingabe(
state = state,
onPferdSucheChanged = viewModel::onPferdSucheChanged,
onPferdSelected = viewModel::onPferdSelected,
onPferdLeeren = viewModel::onPferdLeeren,
onReiterSucheChanged = viewModel::onReiterSucheChanged,
onReiterSelected = viewModel::onReiterSelected,
onReiterLeeren = viewModel::onReiterLeeren,
)
}
HorizontalDivider(
modifier = Modifier.fillMaxHeight().width(1.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// Rechte Hälfte: Verkauf/Buchungen (40%)
Column(
modifier = Modifier
.weight(0.4f)
.fillMaxHeight()
) {
VerkaufBuchungenPanel(
state = state,
onTabChanged = viewModel::onVerkaufTabChanged,
onMengeChanged = viewModel::onVerkaufMengeChanged,
)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// --- Zeile 3: Aktions-Buttons (fix) ---
AktionsButtonLeiste(
canNennen = state.selectedPferd != null && state.selectedReiter != null,
onStartlisteOeffnen = onStartlisteOeffnen,
onErgebnisseOeffnen = onErgebnisseOeffnen,
onAbrechnungOeffnen = onAbrechnungOeffnen,
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// --- Zeile 4: Nennungstabelle + Bewerbsliste (50% Höhe) ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
) {
// Links: Nennungsübersicht (60%)
Column(
modifier = Modifier
.weight(0.6f)
.fillMaxHeight()
) {
NennungenTabelle(
state = state,
nennungen = viewModel.nennungenFuerAktuellen(),
onTabChanged = viewModel::onNennungTabChanged,
onStornieren = viewModel::nennungStornieren,
)
}
HorizontalDivider(
modifier = Modifier.fillMaxHeight().width(1.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// Rechts: Bewerbsliste (40%)
Column(
modifier = Modifier
.weight(0.4f)
.fillMaxHeight()
) {
BewerbslistePanel(
bewerbe = viewModel.gefilterteBewerbe(),
nennungen = state.nennungen,
selectedPferd = state.selectedPferd,
selectedReiter = state.selectedReiter,
spartFilter = state.spartFilter,
onSpartFilterChanged = viewModel::onSpartFilterChanged,
onNennung = { bewerb -> viewModel.nennungDurchfuehren(bewerb) },
)
}
}
}
}
@@ -35,9 +35,6 @@ data class NennungUiState(
val isOnlineLoading: Boolean = false
)
enum class NennungTab { REITER, PFERD, BEWERBE }
enum class VerkaufTab { VERKAUF, BUCHUNGEN }
class NennungViewModel : ViewModel(), KoinComponent {
private val apiClient: HttpClient by inject(named("apiClient"))
@@ -1,832 +0,0 @@
package at.mocode.frontend.features.nennung.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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.*
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.features.nennung.domain.*
import kotlin.time.Duration.Companion.milliseconds
private var lastClickTime: Long = 0L
private var lastClickedBewerb: Int? = null
private fun getCurrentMillis(): Long = 0L // Placeholder for expect/actual or simple helper
private fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return kotlin.math.round(this * multiplier) / multiplier
}
// Farben für Startwunsch-Markierung
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
private val FarbeHinten = Color(0xFFE3F2FD) // Blau
private val FarbeDressur = Color(0xFF3F51B5) // Indigo
private val FarbeSpringen = Color(0xFFE65100) // Orange
@Composable
fun NennungsMaske(
viewModel: NennungViewModel,
onStartlisteOeffnen: () -> Unit = {},
onErgebnisseOeffnen: () -> Unit = {},
onAbrechnungOeffnen: () -> Unit = {},
) {
val state by viewModel.uiState.collectAsState()
// Status-Snackbar
state.statusMeldung?.let { meldung ->
LaunchedEffect(meldung) {
kotlinx.coroutines.delay(3000.milliseconds)
viewModel.statusMeldungDismiss()
}
}
Column(modifier = Modifier.fillMaxSize()) {
// --- Status-Banner ---
state.statusMeldung?.let { meldung ->
Surface(
color = if (meldung.startsWith("")) Color(0xFF388E3C)
else if (meldung.startsWith("⚠️")) Color(0xFFF57C00)
else MaterialTheme.colorScheme.error,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = meldung,
color = Color.White,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelSmall,
)
}
}
// --- Zeile 1: Pferd/Reiter + Verkauf/Buchungen (50% Höhe) ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
) {
// Linke Hälfte: Pferd & Reiter Suche (60%)
Column(
modifier = Modifier
.weight(0.6f)
.fillMaxHeight()
) {
PferdReiterEingabe(
state = state,
onPferdSucheChanged = viewModel::onPferdSucheChanged,
onPferdSelected = viewModel::onPferdSelected,
onPferdLeeren = viewModel::onPferdLeeren,
onReiterSucheChanged = viewModel::onReiterSucheChanged,
onReiterSelected = viewModel::onReiterSelected,
onReiterLeeren = viewModel::onReiterLeeren,
)
}
HorizontalDivider(
modifier = Modifier.fillMaxHeight().width(1.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// Rechte Hälfte: Verkauf/Buchungen (40%)
Column(
modifier = Modifier
.weight(0.4f)
.fillMaxHeight()
) {
VerkaufBuchungenPanel(
state = state,
onTabChanged = viewModel::onVerkaufTabChanged,
onMengeChanged = viewModel::onVerkaufMengeChanged,
)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// --- Zeile 2: Aktions-Buttons (fix) ---
AktionsButtonLeiste(
canNennen = state.selectedPferd != null && state.selectedReiter != null,
onStartlisteOeffnen = onStartlisteOeffnen,
onErgebnisseOeffnen = onErgebnisseOeffnen,
onAbrechnungOeffnen = onAbrechnungOeffnen,
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// --- Zeile 3: Nennungstabelle + Bewerbsliste (50% Höhe) ---
Row(
modifier = Modifier
.fillMaxWidth()
.weight(0.5f)
) {
// Links: Nennungsübersicht (60%)
Column(
modifier = Modifier
.weight(0.6f)
.fillMaxHeight()
) {
NennungenTabelle(
state = state,
nennungen = viewModel.nennungenFuerAktuellen(),
onTabChanged = viewModel::onNennungTabChanged,
onStornieren = viewModel::nennungStornieren,
)
}
HorizontalDivider(
modifier = Modifier.fillMaxHeight().width(1.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// Rechts: Bewerbsliste (40%)
Column(
modifier = Modifier
.weight(0.4f)
.fillMaxHeight()
) {
BewerbslistePanel(
bewerbe = viewModel.gefilterteBewerbe(),
nennungen = state.nennungen,
selectedPferd = state.selectedPferd,
selectedReiter = state.selectedReiter,
spartFilter = state.spartFilter,
onSpartFilterChanged = viewModel::onSpartFilterChanged,
onNennung = { bewerb -> viewModel.nennungDurchfuehren(bewerb) },
)
}
}
}
}
// ---------------------------------------------------------------------------
// Pferd & Reiter Eingabe
// ---------------------------------------------------------------------------
@Composable
private fun PferdReiterEingabe(
state: NennungUiState,
onPferdSucheChanged: (String) -> Unit,
onPferdSelected: (Pferd) -> Unit,
onPferdLeeren: () -> Unit,
onReiterSucheChanged: (String) -> Unit,
onReiterSelected: (Reiter) -> Unit,
onReiterLeeren: () -> Unit,
) {
Row(modifier = Modifier.fillMaxSize().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// --- Pferd ---
Column(modifier = Modifier.weight(1f)) {
SuchfeldMitVorschlaegen(
label = "Pferd:",
value = state.pferdSuche,
onValueChange = onPferdSucheChanged,
onLeeren = onPferdLeeren,
vorschlaege = state.pferdVorschlaege.map { "${it.kopfNr} ${it.name}" },
onVorschlagSelected = { idx -> onPferdSelected(state.pferdVorschlaege[idx]) },
)
state.selectedPferd?.let { pferd ->
MetaDatenBox {
MetaZeile("Rasse:", pferd.rasse)
MetaZeile("Farbe:", pferd.farbe)
MetaZeile("Besitzer:", pferd.besitzer)
if (pferd.stallBox.isNotEmpty()) MetaZeile("Box:", pferd.stallBox)
}
}
Spacer(modifier = Modifier.weight(1f))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 4.dp)) {
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Neu anlegen", fontSize = 10.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Bearbeiten", fontSize = 10.sp)
}
}
}
HorizontalDivider(
modifier = Modifier.fillMaxHeight().width(1.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// --- Reiter ---
Column(modifier = Modifier.weight(1f)) {
SuchfeldMitVorschlaegen(
label = "Reiter:",
value = state.reiterSuche,
onValueChange = onReiterSucheChanged,
onLeeren = onReiterLeeren,
vorschlaege = state.reiterVorschlaege.map { "${it.kopfNr} ${it.vollname}" },
onVorschlagSelected = { idx -> onReiterSelected(state.reiterVorschlaege[idx]) },
)
state.selectedReiter?.let { reiter ->
MetaDatenBox {
MetaZeile("Verein:", reiter.verein)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Lizenz:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(reiter.lizenzNr, fontSize = 10.sp, fontWeight = FontWeight.Medium)
Surface(
color = if (reiter.lizenzGueltig) Color(0xFF388E3C) else MaterialTheme.colorScheme.error,
shape = MaterialTheme.shapes.small,
) {
Text(
text = if (reiter.lizenzGueltig) "Gültig" else "ABGELAUFEN",
color = Color.White,
fontSize = 9.sp,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
)
}
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Konto:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
text = "${reiter.kontoSaldo.round(2)}",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = if (reiter.kontoSaldo < 0) MaterialTheme.colorScheme.error else Color(0xFF388E3C),
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 4.dp)) {
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Neu anlegen", fontSize = 10.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Bearbeiten", fontSize = 10.sp)
}
}
}
}
}
@Composable
private fun SuchfeldMitVorschlaegen(
label: String,
value: String,
onValueChange: (String) -> Unit,
onLeeren: () -> Unit,
vorschlaege: List<String>,
onVorschlagSelected: (Int) -> Unit,
) {
Column {
Text(label, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.weight(1f),
singleLine = true,
placeholder = { Text("Kopfnummer oder Name", fontSize = 11.sp) },
textStyle = LocalTextStyle.current.copy(fontSize = 11.sp),
)
OutlinedButton(
onClick = onLeeren,
modifier = Modifier.height(36.dp),
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Text("Leeren", fontSize = 10.sp)
}
}
if (vorschlaege.isNotEmpty()) {
Surface(shadowElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
LazyColumn(modifier = Modifier.heightIn(max = 120.dp)) {
itemsIndexed(vorschlaege) { idx, vorschlag ->
Text(
text = vorschlag,
fontSize = 11.sp,
modifier = Modifier
.fillMaxWidth()
.clickable { onVorschlagSelected(idx) }
.padding(horizontal = 8.dp, vertical = 4.dp),
)
if (idx < vorschlaege.lastIndex) {
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
}
}
}
}
}
}
}
@Composable
private fun MetaDatenBox(content: @Composable ColumnScope.() -> Unit) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
) {
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(2.dp), content = content)
}
}
@Composable
private fun MetaZeile(label: String, value: String) {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text(label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, fontSize = 10.sp, fontWeight = FontWeight.Medium)
}
}
// ---------------------------------------------------------------------------
// Aktions-Button-Leiste
// ---------------------------------------------------------------------------
@Composable
private fun AktionsButtonLeiste(
canNennen: Boolean,
onStartlisteOeffnen: () -> Unit,
onErgebnisseOeffnen: () -> Unit,
onAbrechnungOeffnen: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 16.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Haupt-Aktion: Nennung durchführen (wird von Bewerbsliste getriggert via Doppelklick)
Surface(
color = if (canNennen) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Default.CheckCircle, contentDescription = null, modifier = Modifier.size(14.dp),
tint = if (canNennen) Color.White else MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Nennung: Doppelklick auf Bewerb [F5]",
fontSize = 10.sp,
color = if (canNennen) Color.White else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Spacer(modifier = Modifier.weight(1f))
SmallActionButton("Startliste", Icons.AutoMirrored.Filled.List, "F7", onStartlisteOeffnen)
SmallActionButton("Ergebnisse", Icons.Default.EmojiEvents, "F8", onErgebnisseOeffnen)
SmallActionButton("Abrechnung", Icons.Default.Receipt, "F9", onAbrechnungOeffnen)
}
}
@Composable
private fun SmallActionButton(
label: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
shortcut: String,
onClick: () -> Unit
) {
OutlinedButton(
onClick = onClick,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
modifier = Modifier.height(28.dp),
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(12.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("$label [$shortcut]", fontSize = 10.sp)
}
}
// ---------------------------------------------------------------------------
// Nennungen-Tabelle (unten links)
// ---------------------------------------------------------------------------
@Composable
private fun NennungenTabelle(
state: NennungUiState,
nennungen: List<Nennung>,
onTabChanged: (NennungTab) -> Unit,
onStornieren: (Nennung) -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
// Tabs
PrimaryTabRow(selectedTabIndex = state.activeNennungTab.ordinal, modifier = Modifier.height(32.dp)) {
NennungTab.entries.forEach { tab ->
Tab(
selected = state.activeNennungTab == tab,
onClick = { onTabChanged(tab) },
modifier = Modifier.height(32.dp),
) {
Text(tab.name, fontSize = 10.sp)
}
}
}
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = "Aktualisieren", modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("${nennungen.size} Nennungen", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Positionieren", fontSize = 10.sp)
}
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Stornieren", fontSize = 10.sp, color = MaterialTheme.colorScheme.error)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Tabellen-Header
TabellenHeader(
listOf("Tag", "Pl.", "Bewerb", "Bewerbsname", "Startwunsch", "Pferd"),
listOf(30f, 25f, 45f, 1f, 70f, 80f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Tabellen-Inhalt
if (nennungen.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Keine Nennungen vorhanden", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(nennungen) { idx, nennung ->
val bgColor = when (nennung.startwunsch) {
Startwunsch.VORNE -> FarbeVorne
Startwunsch.HINTEN -> FarbeHinten
else -> if (idx % 2 == 0) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
Row(
modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(nennung.tag, fontSize = 10.sp, modifier = Modifier.width(30.dp))
Text("${nennung.platz}", fontSize = 10.sp, modifier = Modifier.width(25.dp))
Text("${nennung.bewerbNr}", fontSize = 10.sp, modifier = Modifier.width(45.dp))
Text(
nennung.bewerbName,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
when (nennung.startwunsch) {
Startwunsch.VORNE -> "Vorne"
Startwunsch.HINTEN -> "Hinten"
else -> ""
},
fontSize = 10.sp,
modifier = Modifier.width(70.dp),
)
Text(
nennung.pferdName,
fontSize = 10.sp,
modifier = Modifier.width(80.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
}
}
}
}
// ---------------------------------------------------------------------------
// Bewerbsliste (unten rechts)
// ---------------------------------------------------------------------------
@Composable
private fun BewerbslistePanel(
bewerbe: List<Bewerb>,
nennungen: List<Nennung>,
selectedPferd: Pferd?,
selectedReiter: Reiter?,
spartFilter: Sparte?,
onSpartFilterChanged: (Sparte?) -> Unit,
onNennung: (Bewerb) -> Unit,
) {
val canNennen = selectedPferd != null && selectedReiter != null
var lastClickTime by remember { mutableStateOf(0L) }
var lastClickedBewerb by remember { mutableStateOf<Int?>(null) }
Column(modifier = Modifier.fillMaxSize()) {
// Überschrift + Filter
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text("Bewerbsübersicht", fontSize = 11.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
// Sparte-Filter
FilterChipKlein("Alle", spartFilter == null) { onSpartFilterChanged(null) }
Spacer(modifier = Modifier.width(2.dp))
FilterChipKlein("D", spartFilter == Sparte.DRESSUR) { onSpartFilterChanged(Sparte.DRESSUR) }
Spacer(modifier = Modifier.width(2.dp))
FilterChipKlein("S", spartFilter == Sparte.SPRINGEN) { onSpartFilterChanged(Sparte.SPRINGEN) }
}
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("${bewerbe.size} Bewerbe", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Tabellen-Header
TabellenHeader(
listOf("Tag", "Pl.", "Bewerb", "Beginn", "Nenn.", "Bewerbsname"),
listOf(28f, 22f, 45f, 45f, 35f, 1f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Tabellen-Inhalt
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(bewerbe) { idx, bewerb ->
val bereitsGenannt = canNennen && nennungen.any {
it.bewerbNr == bewerb.nr &&
it.pferdName == selectedPferd.name &&
it.reiterName == selectedReiter.vollname
}
val bgColor = when {
bereitsGenannt -> Color(0xFFBBDEFB) // Blau = bereits gemeldet
idx % 2 == 0 -> Color.Transparent
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
Row(
modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.clickable(enabled = canNennen) {
// Time calculation disabled for Wasm-Main stability test
onNennung(bewerb)
}
.padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(bewerb.tag, fontSize = 10.sp, modifier = Modifier.width(28.dp))
Text("${bewerb.platz}", fontSize = 10.sp, modifier = Modifier.width(22.dp))
// Bewerb-Nr mit Sparte-Farbe
Text(
"${bewerb.nr}",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = if (bewerb.sparte == Sparte.DRESSUR) FarbeDressur else FarbeSpringen,
modifier = Modifier.width(45.dp),
)
Text(bewerb.beginn, fontSize = 10.sp, modifier = Modifier.width(45.dp))
Text("${bewerb.anzahlNennungen}", fontSize = 10.sp, modifier = Modifier.width(35.dp))
Text(
bewerb.name,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
}
if (!canNennen) {
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
Text(
"Bitte wählen Sie zuerst ein Pferd und einen Reiter aus",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth().padding(8.dp),
)
}
}
}
}
@Composable
private fun FilterChipKlein(label: String, selected: Boolean, onClick: () -> Unit) {
Surface(
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
modifier = Modifier.clickable(onClick = onClick),
) {
Text(
label,
fontSize = 9.sp,
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
)
}
}
// ---------------------------------------------------------------------------
// Verkauf & Buchungen Panel (oben rechts)
// ---------------------------------------------------------------------------
@Composable
private fun VerkaufBuchungenPanel(
state: NennungUiState,
onTabChanged: (VerkaufTab) -> Unit,
onMengeChanged: (VerkaufArtikel, Int) -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
PrimaryTabRow(selectedTabIndex = state.activeVerkaufTab.ordinal, modifier = Modifier.height(32.dp)) {
VerkaufTab.entries.forEach { tab ->
Tab(
selected = state.activeVerkaufTab == tab,
onClick = { onTabChanged(tab) },
modifier = Modifier.height(32.dp),
) {
Text(tab.name, fontSize = 10.sp)
}
}
}
when (state.activeVerkaufTab) {
VerkaufTab.VERKAUF -> VerkaufTabInhalt(state.verkaufArtikel, onMengeChanged)
VerkaufTab.BUCHUNGEN -> BuchungenTabInhalt()
}
}
}
@Composable
private fun VerkaufTabInhalt(artikel: List<VerkaufArtikel>, onMengeChanged: (VerkaufArtikel, Int) -> Unit) {
Column(modifier = Modifier.fillMaxSize()) {
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("${artikel.size} Artikel", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Rückgängig", fontSize = 10.sp)
}
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Speichern", fontSize = 10.sp)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
TabellenHeader(
listOf("KNr", "+", "Menge", "", "Buchungstext", "Betrag", "Gebucht"),
listOf(30f, 20f, 45f, 20f, 1f, 55f, 55f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(artikel) { idx, art ->
val bgColor = when {
art.buchungstext == "Belastung" || art.buchungstext == "Gutschrift" -> Color(0xFFFFFDE7)
idx % 2 == 0 -> Color.Transparent
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
Row(
modifier = Modifier.fillMaxWidth().background(bgColor).padding(horizontal = 4.dp, vertical = 1.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(art.knr, fontSize = 10.sp, modifier = Modifier.width(30.dp))
IconButton(onClick = { onMengeChanged(art, 1) }, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Add, contentDescription = "+", modifier = Modifier.size(12.dp))
}
Text("${art.menge}", fontSize = 10.sp, modifier = Modifier.width(45.dp), fontWeight = FontWeight.Medium)
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Remove, contentDescription = "", modifier = Modifier.size(12.dp))
}
Text(
art.buchungstext,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text("${art.betrag.round(2)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
Text("${art.gebucht.round(2)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
}
}
}
@Composable
private fun BuchungenTabInhalt() {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("0 Buchungen", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Stornieren", fontSize = 10.sp, color = MaterialTheme.colorScheme.error)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
TabellenHeader(listOf("Kopfnr", "Menge", "Buchungstext", "Soll", "Haben"), listOf(55f, 45f, 1f, 55f, 55f))
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Keine Buchungen vorhanden", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
// ---------------------------------------------------------------------------
// Hilfs-Composable: Tabellen-Header
// ---------------------------------------------------------------------------
@Composable
private fun TabellenHeader(spalten: List<String>, breiten: List<Float>) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
spalten.forEachIndexed { idx, name ->
val breite = breiten.getOrNull(idx) ?: 1f
if (breite == 1f) {
Text(name, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
} else {
Text(name, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(breite.dp))
}
}
}
}
@@ -0,0 +1,80 @@
package at.mocode.frontend.features.nennung.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.EmojiEvents
import androidx.compose.material.icons.filled.Receipt
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.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun AktionsButtonLeiste(
canNennen: Boolean,
onStartlisteOeffnen: () -> Unit,
onErgebnisseOeffnen: () -> Unit,
onAbrechnungOeffnen: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 16.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Haupt-Aktion: Nennung durchführen (wird von Bewerbsliste getriggert via Doppelklick)
Surface(
color = if (canNennen) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Default.CheckCircle, contentDescription = null, modifier = Modifier.size(14.dp),
tint = if (canNennen) Color.White else MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Nennung: Doppelklick auf Bewerb [F5]",
fontSize = 10.sp,
color = if (canNennen) Color.White else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
Spacer(modifier = Modifier.weight(1f))
SmallActionButton("Startliste", Icons.AutoMirrored.Filled.List, "F7", onStartlisteOeffnen)
SmallActionButton("Ergebnisse", Icons.Default.EmojiEvents, "F8", onErgebnisseOeffnen)
SmallActionButton("Abrechnung", Icons.Default.Receipt, "F9", onAbrechnungOeffnen)
}
}
@Composable
private fun SmallActionButton(
label: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
shortcut: String,
onClick: () -> Unit
) {
OutlinedButton(
onClick = onClick,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
modifier = Modifier.height(28.dp),
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(12.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("$label [$shortcut]", fontSize = 10.sp)
}
}
@@ -0,0 +1,219 @@
package at.mocode.frontend.features.nennung.presentation.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import at.mocode.frontend.features.nennung.domain.Pferd
import at.mocode.frontend.features.nennung.domain.Reiter
import at.mocode.frontend.features.nennung.presentation.NennungUiState
@Composable
fun PferdReiterEingabe(
state: NennungUiState,
onPferdSucheChanged: (String) -> Unit,
onPferdSelected: (Pferd) -> Unit,
onPferdLeeren: () -> Unit,
onReiterSucheChanged: (String) -> Unit,
onReiterSelected: (Reiter) -> Unit,
onReiterLeeren: () -> Unit,
) {
Row(modifier = Modifier.fillMaxSize().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// --- Pferd ---
Column(modifier = Modifier.weight(1f)) {
SuchfeldMitVorschlaegen(
label = "Pferd:",
value = state.pferdSuche,
onValueChange = onPferdSucheChanged,
onLeeren = onPferdLeeren,
vorschlaege = state.pferdVorschlaege.map { "${it.kopfNr} ${it.name}" },
onVorschlagSelected = { idx -> onPferdSelected(state.pferdVorschlaege[idx]) },
)
state.selectedPferd?.let { pferd ->
MetaDatenBox {
MetaZeile("Rasse:", pferd.rasse)
MetaZeile("Farbe:", pferd.farbe)
MetaZeile("Besitzer:", pferd.besitzer)
if (pferd.stallBox.isNotEmpty()) MetaZeile("Box:", pferd.stallBox)
}
}
Spacer(modifier = Modifier.weight(1f))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 4.dp)) {
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Neu anlegen", fontSize = 10.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Bearbeiten", fontSize = 10.sp)
}
}
}
HorizontalDivider(
modifier = Modifier.fillMaxHeight().width(1.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// --- Reiter ---
Column(modifier = Modifier.weight(1f)) {
SuchfeldMitVorschlaegen(
label = "Reiter:",
value = state.reiterSuche,
onValueChange = onReiterSucheChanged,
onLeeren = onReiterLeeren,
vorschlaege = state.reiterVorschlaege.map { "${it.kopfNr} ${it.vollname}" },
onVorschlagSelected = { idx -> onReiterSelected(state.reiterVorschlaege[idx]) },
)
state.selectedReiter?.let { reiter ->
MetaDatenBox {
MetaZeile("Verein:", reiter.verein)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Lizenz:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(reiter.lizenzNr, fontSize = 10.sp, fontWeight = FontWeight.Medium)
Surface(
color = if (reiter.lizenzGueltig) Color(0xFF388E3C) else MaterialTheme.colorScheme.error,
shape = MaterialTheme.shapes.small,
) {
Text(
text = if (reiter.lizenzGueltig) "Gültig" else "ABGELAUFEN",
color = Color.White,
fontSize = 9.sp,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
)
}
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Konto:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
text = "${(kotlin.math.round(reiter.kontoSaldo * 100) / 100.0)}",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = if (reiter.kontoSaldo < 0) MaterialTheme.colorScheme.error else Color(0xFF388E3C),
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 4.dp)) {
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Neu anlegen", fontSize = 10.sp)
}
OutlinedButton(
onClick = {},
modifier = Modifier.weight(1f).height(28.dp),
contentPadding = PaddingValues(0.dp)
) {
Text("Bearbeiten", fontSize = 10.sp)
}
}
}
}
}
@Composable
fun SuchfeldMitVorschlaegen(
label: String,
value: String,
onValueChange: (String) -> Unit,
onLeeren: () -> Unit,
vorschlaege: List<String>,
onVorschlagSelected: (Int) -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(label, fontSize = 11.sp) },
textStyle = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
singleLine = true,
leadingIcon = { Icon(Icons.Default.Search, null, modifier = Modifier.size(16.dp)) },
trailingIcon = {
if (value.isNotEmpty()) {
IconButton(onClick = onLeeren, modifier = Modifier.size(16.dp)) {
Icon(Icons.Default.Clear, null)
}
}
},
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
)
)
if (vorschlaege.isNotEmpty()) {
Popup(alignment = Alignment.TopStart) {
Surface(
modifier = Modifier.width(300.dp).heightIn(max = 200.dp),
tonalElevation = 8.dp,
shadowElevation = 4.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
) {
LazyColumn {
itemsIndexed(vorschlaege) { idx, vorschlag ->
Text(
text = vorschlag,
modifier = Modifier
.fillMaxWidth()
.clickable { onVorschlagSelected(idx) }
.padding(8.dp),
style = MaterialTheme.typography.bodySmall
)
if (idx < vorschlaege.size - 1) {
HorizontalDivider()
}
}
}
}
}
}
}
}
@Composable
private fun MetaDatenBox(content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), MaterialTheme.shapes.small)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
content = content
)
}
@Composable
private fun MetaZeile(label: String, value: String) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text(label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, fontSize = 10.sp, fontWeight = FontWeight.Medium)
}
}
@@ -0,0 +1,102 @@
package at.mocode.frontend.features.nennung.presentation.online
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.Download
import androidx.compose.material.icons.filled.Refresh
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.features.nennung.domain.OnlineNennung
import at.mocode.frontend.features.nennung.presentation.NennungUiState
@Composable
fun OnlineNennungEingang(
state: NennungUiState,
onRefresh: () -> Unit,
onUebernehmen: (OnlineNennung) -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("Online-Nennungen (Eingang)", style = MaterialTheme.typography.titleSmall)
Spacer(modifier = Modifier.weight(1f))
if (state.isOnlineLoading) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
} else {
IconButton(onClick = onRefresh, modifier = Modifier.size(24.dp)) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh", modifier = Modifier.size(16.dp))
}
}
}
HorizontalDivider()
if (state.onlineNennungen.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
if (state.isOnlineLoading) "Lade Daten..." else "Keine neuen Online-Nennungen",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.onlineNennungen) { nennung ->
OnlineNennungItem(nennung, onUebernehmen)
HorizontalDivider(thickness = 0.5.dp)
}
}
}
}
}
@Composable
private fun OnlineNennungItem(
nennung: OnlineNennung,
onUebernehmen: (OnlineNennung) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(
"${nennung.vorname} ${nennung.nachname}",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold
)
Text(
"Pferd: ${nennung.pferdName} (${nennung.pferdAlter} J.)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Bewerbe: ${nennung.bewerbe}",
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Button(
onClick = { onUebernehmen(nennung) },
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
modifier = Modifier.height(28.dp)
) {
Icon(Icons.Default.Download, null, modifier = Modifier.size(12.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("Übernehmen", fontSize = 10.sp)
}
}
}
@@ -0,0 +1,256 @@
package at.mocode.frontend.features.nennung.presentation.tabs
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.features.nennung.domain.*
import at.mocode.frontend.features.nennung.presentation.NennungUiState
// Farben für Startwunsch-Markierung (aus NennungsMaske.kt)
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
private val FarbeHinten = Color(0xFFE3F2FD) // Blau
private val FarbeDressur = Color(0xFF3F51B5) // Indigo
private val FarbeSpringen = Color(0xFFE65100) // Orange
@Composable
fun NennungenTabelle(
state: NennungUiState,
nennungen: List<Nennung>,
onTabChanged: (NennungTab) -> Unit,
onStornieren: (Nennung) -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
// Tabs
@OptIn(ExperimentalMaterial3Api::class)
PrimaryTabRow(selectedTabIndex = state.activeNennungTab.ordinal, modifier = Modifier.height(32.dp)) {
NennungTab.entries.forEach { tab ->
Tab(
selected = state.activeNennungTab == tab,
onClick = { onTabChanged(tab) },
modifier = Modifier.height(32.dp),
) {
Text(tab.name, fontSize = 10.sp)
}
}
}
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = "Aktualisieren", modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("${nennungen.size} Nennungen", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Positionieren", fontSize = 10.sp)
}
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Stornieren", fontSize = 10.sp, color = MaterialTheme.colorScheme.error)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Tabellen-Header
TabellenHeader(
listOf("Tag", "Pl.", "Bewerb", "Bewerbsname", "Startwunsch", "Pferd"),
listOf(30f, 25f, 45f, 1f, 70f, 80f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Tabellen-Inhalt
if (nennungen.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Keine Nennungen vorhanden", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(nennungen) { idx, nennung ->
val bgColor = when (nennung.startwunsch) {
Startwunsch.VORNE -> FarbeVorne
Startwunsch.HINTEN -> FarbeHinten
else -> if (idx % 2 == 0) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
Row(
modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(nennung.tag, fontSize = 10.sp, modifier = Modifier.width(30.dp))
Text("${nennung.platz}", fontSize = 10.sp, modifier = Modifier.width(25.dp))
Text("${nennung.bewerbNr}", fontSize = 10.sp, modifier = Modifier.width(45.dp))
Text(
nennung.bewerbName,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
when (nennung.startwunsch) {
Startwunsch.VORNE -> "Vorne"
Startwunsch.HINTEN -> "Hinten"
else -> ""
},
fontSize = 10.sp,
modifier = Modifier.width(70.dp),
)
Text(
nennung.pferdName,
fontSize = 10.sp,
modifier = Modifier.width(80.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
}
}
}
}
@Composable
fun BewerbslistePanel(
bewerbe: List<Bewerb>,
nennungen: List<Nennung>,
selectedPferd: Pferd?,
selectedReiter: Reiter?,
spartFilter: Sparte?,
onSpartFilterChanged: (Sparte?) -> Unit,
onNennung: (Bewerb) -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
// Filter-Leiste
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text("Filter:", fontSize = 10.sp, fontWeight = FontWeight.Bold)
FilterChipKlein("Alle", spartFilter == null) { onSpartFilterChanged(null) }
FilterChipKlein("Dressur", spartFilter == Sparte.DRESSUR) { onSpartFilterChanged(Sparte.DRESSUR) }
FilterChipKlein("Springen", spartFilter == Sparte.SPRINGEN) { onSpartFilterChanged(Sparte.SPRINGEN) }
Spacer(modifier = Modifier.weight(1f))
Text("${bewerbe.size} Bewerbe", fontSize = 10.sp)
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
TabellenHeader(
listOf("Nr", "Tag", "Pl.", "Zeit", "Bewerbsbezeichnung", "S", "Kl.", "N"),
listOf(25f, 30f, 25f, 40f, 1f, 20f, 25f, 25f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(bewerbe) { idx, bew ->
val bereitsGenannt = nennungen.any { it.bewerbNr == bew.nr && it.pferdName == selectedPferd?.name && it.reiterName == selectedReiter?.vollname }
val bgColor = if (idx % 2 == 0) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
Row(
modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.clickable(enabled = !bereitsGenannt && selectedPferd != null && selectedReiter != null) { onNennung(bew) }
.padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text("${bew.nr}", fontSize = 10.sp, modifier = Modifier.width(25.dp), fontWeight = FontWeight.Bold)
Text(bew.tag, fontSize = 10.sp, modifier = Modifier.width(30.dp))
Text("${bew.platz}", fontSize = 10.sp, modifier = Modifier.width(25.dp))
Text(bew.beginn, fontSize = 10.sp, modifier = Modifier.width(40.dp))
Text(
bew.name,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (bereitsGenannt) MaterialTheme.colorScheme.primary else Color.Unspecified
)
Text(
text = if (bew.sparte == Sparte.DRESSUR) "D" else "S",
fontSize = 9.sp,
modifier = Modifier.width(20.dp),
color = if (bew.sparte == Sparte.DRESSUR) FarbeDressur else FarbeSpringen,
fontWeight = FontWeight.Black
)
Text(bew.klasse, fontSize = 10.sp, modifier = Modifier.width(25.dp))
Text("${bew.anzahlNennungen}", fontSize = 10.sp, modifier = Modifier.width(25.dp), textAlign = androidx.compose.ui.text.style.TextAlign.End)
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
}
}
}
@Composable
private fun FilterChipKlein(label: String, selected: Boolean, onClick: () -> Unit) {
Surface(
selected = selected,
onClick = onClick,
shape = MaterialTheme.shapes.small,
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
border = if (selected) null else BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant)
) {
Text(
label,
fontSize = 9.sp,
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
)
}
}
@Composable
private fun TabellenHeader(spalten: List<String>, breiten: List<Float>) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.padding(horizontal = 4.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
spalten.forEachIndexed { idx, label ->
val modifier = if (breiten[idx] == 1f) Modifier.weight(1f) else Modifier.width(breiten[idx].dp)
Text(
text = label,
modifier = modifier,
fontSize = 9.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@@ -0,0 +1,169 @@
package at.mocode.frontend.features.nennung.presentation.tabs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Remove
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.features.nennung.domain.*
import at.mocode.frontend.features.nennung.presentation.NennungUiState
@Composable
fun VerkaufBuchungenPanel(
state: NennungUiState,
onTabChanged: (VerkaufTab) -> Unit,
onMengeChanged: (VerkaufArtikel, Int) -> Unit,
) {
Column(modifier = Modifier.fillMaxSize()) {
@OptIn(ExperimentalMaterial3Api::class)
PrimaryTabRow(selectedTabIndex = state.activeVerkaufTab.ordinal, modifier = Modifier.height(32.dp)) {
VerkaufTab.entries.forEach { tab ->
Tab(
selected = state.activeVerkaufTab == tab,
onClick = { onTabChanged(tab) },
modifier = Modifier.height(32.dp),
) {
Text(tab.name, fontSize = 10.sp)
}
}
}
when (state.activeVerkaufTab) {
VerkaufTab.VERKAUF -> VerkaufTabInhalt(state.verkaufArtikel, onMengeChanged)
VerkaufTab.BUCHUNGEN -> BuchungenTabInhalt()
}
}
}
@Composable
private fun VerkaufTabInhalt(artikel: List<VerkaufArtikel>, onMengeChanged: (VerkaufArtikel, Int) -> Unit) {
Column(modifier = Modifier.fillMaxSize()) {
// Toolbar
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("${artikel.size} Artikel", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Rückgängig", fontSize = 10.sp)
}
TextButton(
onClick = {},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text("Speichern", fontSize = 10.sp)
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
TabellenHeader(
listOf("KNr", "+", "Menge", "", "Buchungstext", "Betrag", "Gebucht"),
listOf(30f, 20f, 45f, 20f, 1f, 55f, 55f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(artikel) { idx, art ->
val bgColor = when {
art.buchungstext == "Belastung" || art.buchungstext == "Gutschrift" -> Color(0xFFFFFDE7)
idx % 2 == 0 -> Color.Transparent
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
}
Row(
modifier = Modifier.fillMaxWidth().background(bgColor).padding(horizontal = 4.dp, vertical = 1.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(art.knr, fontSize = 10.sp, modifier = Modifier.width(30.dp))
IconButton(onClick = { onMengeChanged(art, 1) }, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Add, contentDescription = "+", modifier = Modifier.size(12.dp))
}
Text("${art.menge}", fontSize = 10.sp, modifier = Modifier.width(45.dp), fontWeight = FontWeight.Medium)
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Remove, contentDescription = "", modifier = Modifier.size(12.dp))
}
Text(
art.buchungstext,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text("${(kotlin.math.round(art.betrag * 100) / 100.0)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
Text("${(kotlin.math.round(art.gebucht * 100) / 100.0)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
}
}
}
@Composable
private fun BuchungenTabInhalt() {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
}
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
Spacer(modifier = Modifier.weight(1f))
Text("0 Buchungen", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
TabellenHeader(
listOf("Datum", "Uhrzeit", "User", "Buchungstext", "Betrag", "G", "Z"),
listOf(60f, 50f, 40f, 1f, 55f, 20f, 20f)
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Keine Buchungen vorhanden", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
@Composable
private fun TabellenHeader(spalten: List<String>, breiten: List<Float>) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.padding(horizontal = 4.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
spalten.forEachIndexed { idx, label ->
val modifier = if (breiten[idx] == 1f) Modifier.weight(1f) else Modifier.width(breiten[idx].dp)
Text(
text = label,
modifier = modifier,
fontSize = 9.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}