Refine MsTextField component: introduce compact mode, enhance visual styling and error handling, and improve placeholder and keyboard interaction logic. Add Dimens and Colors updates, implement navigation rail and header layout for the desktop shell, and update ROADMAP documentation with planned phases.

This commit is contained in:
2026-04-12 23:06:49 +02:00
parent 5eb2dd6904
commit 126522e606
17 changed files with 854 additions and 551 deletions
@@ -1,6 +1,7 @@
package at.mocode.veranstaltung.feature.presentation
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -8,17 +9,19 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
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.components.MsTextField
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens
// Status-Farben gemäß Vision_03
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
@@ -52,18 +55,67 @@ fun AdminUebersichtScreen(
ort = "4221 NEUMARKT/M.",
datum = "12.13.04.2026",
turnierAnzahl = 2,
nennungen = 0,
nennungen = 142,
letzteAktivitaet = "vor 1 Min",
status = VeranstaltungStatus.VORBEREITUNG,
turniere = listOf(
TurnierUiModel(id = 26129, nummer = 26129, name = "CDN-C-NEU CDNP-C-NEU", bewerbAnzahl = 16),
TurnierUiModel(id = 26128, nummer = 26128, name = "CSN-C-NEU CSNP-C-NEU", bewerbAnzahl = 18),
)
),
VeranstaltungUiModel(
id = 1002,
name = "LINZ-EBELSBERG",
ort = "4030 LINZ",
datum = "15.18.05.2026",
turnierAnzahl = 1,
nennungen = 89,
letzteAktivitaet = "vor 2 Std",
status = VeranstaltungStatus.LIVE,
turniere = listOf(
TurnierUiModel(id = 26130, nummer = 26130, name = "CSN-B", bewerbAnzahl = 24),
)
)
)
val veranstaltungen = remember { mutableStateListOf<VeranstaltungUiModel>().also { it.addAll(sample) } }
Column(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(Dimens.SpacingL)
) {
// Page Header
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = Dimens.SpacingL),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "Veranstaltungs-Verwaltung",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
Text(
text = "Übersicht aller laufenden und geplanten Reitsport-Veranstaltungen",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Button(
onClick = onVeranstalterAuswahl,
shape = MaterialTheme.shapes.medium,
contentPadding = PaddingValues(horizontal = Dimens.SpacingM, vertical = Dimens.SpacingS)
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeS))
Spacer(Modifier.width(Dimens.SpacingS))
Text("Neue Veranstaltung")
}
}
// KPI-Kacheln
KpiKachelRow(
liveAktiv = 0,
@@ -75,39 +127,42 @@ fun AdminUebersichtScreen(
onCupsClick = onCupsOeffnen
)
// Toolbar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
Spacer(Modifier.height(Dimens.SpacingM))
// Toolbar (Suche & Filter)
Surface(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp
) {
Button(
onClick = onVeranstalterAuswahl,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
Row(
modifier = Modifier
.fillMaxWidth()
.padding(Dimens.SpacingM),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neue Veranstaltung")
MsTextField(
value = "",
onValueChange = {},
placeholder = "Suche nach Name, Ort oder Turnier-Nr.",
leadingIcon = Icons.Default.Search,
modifier = Modifier.weight(1f),
singleLine = true,
)
// Status-Filter Chips
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
StatusFilterChip("Alle", selected = true)
StatusFilterChip("Vorbereitung", selected = false)
StatusFilterChip("Live", selected = false)
StatusFilterChip("Abgeschlossen", selected = false)
}
}
OutlinedTextField(
value = "",
onValueChange = {},
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 13.sp) },
modifier = Modifier.weight(1f).height(48.dp),
singleLine = true,
)
// Status-Filter Chips
StatusFilterChip("Alle", selected = true)
StatusFilterChip("Vorbereitung", selected = false)
StatusFilterChip("Live", selected = false)
StatusFilterChip("Abgeschlossen", selected = false)
}
HorizontalDivider()
Spacer(Modifier.height(Dimens.SpacingM))
// Veranstaltungs-Liste
if (veranstaltungen.isEmpty()) {
@@ -130,19 +185,18 @@ fun AdminUebersichtScreen(
Spacer(Modifier.height(16.dp))
Button(
onClick = onVeranstalterAuswahl,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeS))
Spacer(Modifier.width(Dimens.SpacingXS))
Text("Neue Veranstaltung anlegen")
}
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
contentPadding = PaddingValues(bottom = Dimens.SpacingL),
) {
items(items = veranstaltungen, key = { it.id }) { veranstaltung ->
VeranstaltungCard(
@@ -249,12 +303,12 @@ private fun VeranstaltungCard(
onOeffnen: () -> Unit,
onLoeschen: () -> Unit,
) {
Card(
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
shape = MaterialTheme.shapes.medium,
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(modifier = Modifier.padding(16.dp)) {
Column(modifier = Modifier.padding(Dimens.SpacingM)) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
@@ -264,16 +318,16 @@ private fun VeranstaltungCard(
Column(modifier = Modifier.weight(1f)) {
Text(
text = veranstaltung.name,
fontWeight = FontWeight.SemiBold,
fontSize = 15.sp,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(top = 2.dp),
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
modifier = Modifier.padding(top = Dimens.SpacingXS),
) {
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
LabelValue("📍", veranstaltung.ort)
LabelValue("📅", veranstaltung.datum)
LabelValue("🏆", "${veranstaltung.turnierAnzahl} Turniere")
}
}
StatusBadge(veranstaltung.status)
@@ -281,56 +335,77 @@ private fun VeranstaltungCard(
// Turnier-Liste
if (veranstaltung.turniere.isNotEmpty()) {
Spacer(Modifier.height(8.dp))
Text("Turniere (${veranstaltung.turniere.size}):", fontSize = 12.sp, fontWeight = FontWeight.Medium)
veranstaltung.turniere.forEach { turnier ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
shape = MaterialTheme.shapes.small,
color = Color(0xFF1E3A8A),
Spacer(Modifier.height(Dimens.SpacingM))
Surface(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
shape = MaterialTheme.shapes.small
) {
Column(modifier = Modifier.padding(Dimens.SpacingS)) {
Text(
text = "Zugeordnete Turniere",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = Dimens.SpacingXS)
)
veranstaltung.turniere.forEach { turnier ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = turnier.nummer.toString(),
color = Color.White,
fontSize = 11.sp,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.primaryContainer,
) {
Text(
text = turnier.nummer.toString(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
)
}
Text(
text = "${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)",
style = MaterialTheme.typography.bodySmall
)
}
TextButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
Text("Details", style = MaterialTheme.typography.labelSmall)
}
}
Text("${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)", fontSize = 12.sp)
}
OutlinedButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
Text("Zum Turnier", fontSize = 11.sp)
}
}
}
}
// Footer
Spacer(Modifier.height(8.dp))
Spacer(Modifier.height(Dimens.SpacingM))
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
Spacer(Modifier.height(Dimens.SpacingS))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Nennungen: ${veranstaltung.nennungen}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("Letzte Aktivität: ${veranstaltung.letzteAktivitaet}", fontSize = 12.sp, color = Color(0xFF6B7280))
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
LabelValue("Nennungen:", veranstaltung.nennungen.toString())
LabelValue("Aktivität:", veranstaltung.letzteAktivitaet)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
}
Button(
onClick = onOeffnen,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
modifier = Modifier.height(32.dp),
shape = MaterialTheme.shapes.small,
modifier = Modifier.height(36.dp),
) {
Text("Zur Veranstaltung", fontSize = 12.sp)
}
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626))
Text("Veranstaltung öffnen", style = MaterialTheme.typography.labelMedium)
}
}
}
@@ -338,6 +413,15 @@ private fun VeranstaltungCard(
}
}
@Composable
private fun LabelValue(label: String, value: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(label, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.width(4.dp))
Text(value, style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium)
}
}
@Composable
private fun StatusBadge(status: VeranstaltungStatus) {
val (text, color) = when (status) {