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
@@ -41,20 +41,16 @@ fun <T> MsSearchableSelect(
Column(modifier = modifier) {
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
OutlinedTextField(
MsTextField(
value = selectedOption?.let { optionLabel(it) } ?: "",
onValueChange = {},
readOnly = true,
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled) { showDialog = true },
label = label,
placeholder = placeholder,
leadingIcon = Icons.Default.Search,
modifier = modifier.clickable(enabled = enabled) { showDialog = true },
enabled = enabled,
singleLine = true,
textStyle = MaterialTheme.typography.bodyMedium,
shape = MaterialTheme.shapes.small
)
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
@@ -75,17 +71,16 @@ fun <T> MsSearchableSelect(
)
// Internes Suchfeld im Dialog
OutlinedTextField(
MsTextField(
value = searchText,
onValueChange = {
searchText = it
onSearchQueryChange(it)
},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Suchbegriff eingeben...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
placeholder = "Suchbegriff eingeben...",
leadingIcon = Icons.Default.Search,
singleLine = true,
shape = MaterialTheme.shapes.small
)
Spacer(modifier = Modifier.height(16.dp))
@@ -1,12 +1,11 @@
package at.mocode.frontend.core.designsystem.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.input.ImeAction
@@ -14,6 +13,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.theme.Dimens
@Composable
fun MsTextField(
@@ -31,28 +31,41 @@ fun MsTextField(
enabled: Boolean = true,
readOnly: Boolean = false,
singleLine: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
visualTransformation: VisualTransformation = VisualTransformation.None
visualTransformation: VisualTransformation = VisualTransformation.None,
compact: Boolean = true // Desktop-optimiert (kompakter)
) {
val height = if (compact) Dimens.TextFieldHeight else Dimens.TextFieldHeightL
Column(modifier = modifier) {
if (label != null) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 4.dp, start = 4.dp)
)
}
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = label?.let { { Text(it) } },
placeholder = placeholder?.let { { Text(it) } },
modifier = Modifier
.fillMaxWidth()
.heightIn(min = height),
placeholder = placeholder?.let { { Text(it, style = MaterialTheme.typography.bodyMedium) } },
leadingIcon = leadingIcon?.let { icon ->
{ Icon(imageVector = icon, contentDescription = null) }
{ Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM)) }
},
trailingIcon = if (trailingIcon != null) {
{
IconButton(
onClick = onTrailingIconClick ?: {}
) {
Icon(imageVector = trailingIcon, contentDescription = null)
Icon(imageVector = trailingIcon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM))
}
}
} else null,
@@ -61,6 +74,15 @@ fun MsTextField(
readOnly = readOnly,
singleLine = singleLine,
maxLines = maxLines,
textStyle = MaterialTheme.typography.bodyMedium,
shape = MaterialTheme.shapes.small,
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
),
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = imeAction
@@ -70,24 +92,20 @@ fun MsTextField(
)
// Error or helper text
when {
isError && errorMessage != null -> {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
helperText != null -> {
Text(
text = helperText,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
if (isError && errorMessage != null) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
)
} else if (helperText != null) {
Text(
text = helperText,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
)
}
}
}
@@ -14,12 +14,16 @@ object AppColors {
val PrimaryContainer = Color(0xFFDEEBFF)
val OnPrimaryContainer = Color(0xFF0052CC)
// Subtiles Sidebar-Grau / Navigation
val NavigationSurface = Color(0xFFF4F5F7)
val NavigationContent = Color(0xFF42526E)
// Helleres Blau für sekundäre Akzente
val Secondary = Color(0xFF2684FF)
val OnSecondary = Color.White
// Neutral- & Hintergrund (Light Mode)
val BackgroundLight = Color(0xFFF4F5F7) // Helles Grau (nicht hartes Weiß)
val BackgroundLight = Color(0xFFF9FAFB) // Sehr helles Grau für den Content Bereich
val SurfaceLight = Color.White
val OnBackgroundLight = Color(0xFF172B4D) // Fast Schwarz (besser lesbar)
@@ -13,10 +13,17 @@ object Dimens {
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
val SpacingM = 16.dp // Abstand für Sektionen
val SpacingL = 24.dp // Außenabstand für Screens
val SpacingXL = 32.dp
// Navigations-Maße
val NavRailWidth = 72.dp
val NavRailExpandedWidth = 240.dp
val TopBarHeight = 56.dp
// Sizes (Größen)
val IconSizeS = 16.dp
val IconSizeM = 24.dp
val IconSizeL = 32.dp
// Borders
val BorderThin = 1.dp
@@ -24,4 +31,10 @@ object Dimens {
// Corner Radius (Ecken)
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
val CornerRadiusM = 8.dp
val CornerRadiusL = 12.dp
// Form-Elemente (Eingabefelder, Buttons)
val TextFieldHeight = 44.dp // Kompakte Höhe für Desktop-Enterprise-Apps
val TextFieldHeightL = 56.dp // Standard Material Höhe (für prominente Felder)
val ButtonHeight = 40.dp
}
@@ -3,8 +3,39 @@ CREATE TABLE LocalSettings (
value TEXT NOT NULL
);
-- SyncEvents Tabelle für Offline-First/Event-Sourcing (ADR-0022/Concept)
CREATE TABLE SyncEvents (
sequence_number INTEGER NOT NULL,
origin_node_id TEXT NOT NULL,
event_id TEXT NOT NULL,
turnier_id TEXT,
aggregate_type TEXT NOT NULL,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
created_at INTEGER NOT NULL,
schema_version INTEGER NOT NULL DEFAULT 1,
checksum TEXT,
synced_at INTEGER, -- Null if not yet synced to backend
PRIMARY KEY (origin_node_id, sequence_number)
);
insertOrReplace:
INSERT OR REPLACE INTO LocalSettings(key, value) VALUES (?, ?);
selectAll:
SELECT * FROM LocalSettings;
-- SyncEvents Queries
insertSyncEvent:
INSERT INTO SyncEvents(sequence_number, origin_node_id, event_id, turnier_id, aggregate_type, aggregate_id, event_type, payload, created_at, schema_version, checksum)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
selectUnsyncedEvents:
SELECT * FROM SyncEvents WHERE synced_at IS NULL ORDER BY sequence_number ASC;
markSynced:
UPDATE SyncEvents SET synced_at = ? WHERE origin_node_id = ? AND sequence_number = ?;
getLastSequenceNumber:
SELECT MAX(sequence_number) FROM SyncEvents WHERE origin_node_id = ?;
@@ -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) {
@@ -4,20 +4,20 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Chat
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material.icons.automirrored.filled.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.features.billing.presentation.BillingScreen
import at.mocode.frontend.features.billing.presentation.BillingViewModel
@@ -49,11 +49,9 @@ private val TopBarTextColor = Color.White
* Haupt-Layout der Desktop-App gemäß Vision_03.
*
* Struktur:
* - TopBar (dunkelblau): App-Titel + Breadcrumb + Logout
* - NavigationRail (links): Globale Navigation
* - Header (oben): Breadcrumb + Status + Logout
* - Content: kontextabhängiger Screen
*
* Kein Nav-Rail, keine Sidebar Navigation erfolgt über
* Breadcrumb-Klicks und horizontale Tabs innerhalb der Screens.
*/
@Composable
fun DesktopMainLayout(
@@ -62,18 +60,25 @@ fun DesktopMainLayout(
onBack: () -> Unit,
onLogout: () -> Unit,
) {
// Onboarding-Eingaben zwischen Navigationswechseln behalten → State hier (außerhalb des when) hosten
// Onboarding-Eingaben zwischen Navigationswechseln behalten
var obGeraet by rememberSaveable { mutableStateOf("") }
var obKey by rememberSaveable { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize()) {
DesktopTopBar(
Row(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
// Navigation Rail (Modernere Seitenleiste)
DesktopNavRail(
currentScreen = currentScreen,
onNavigate = onNavigate,
onBack = onBack,
onLogout = onLogout,
onNavigate = onNavigate
)
Column(modifier = Modifier.fillMaxSize()) {
DesktopTopHeader(
currentScreen = currentScreen,
onNavigate = onNavigate,
onBack = onBack,
onLogout = onLogout,
)
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
DesktopContentArea(
currentScreen = currentScreen,
@@ -85,247 +90,316 @@ fun DesktopMainLayout(
onObKeyChange = { obKey = it },
)
}
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
DesktopFooterBar()
}
}
}
/**
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
*
* Breadcrumb-Logik:
* - Root: "🏠 Admin - Verwaltung"
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
*/
@Composable
private fun DesktopNavRail(
currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit
) {
Surface(
modifier = Modifier.fillMaxHeight().width(Dimens.NavRailWidth),
color = AppColors.NavigationSurface,
contentColor = AppColors.NavigationContent,
) {
Column(
modifier = Modifier.fillMaxHeight().padding(vertical = Dimens.SpacingM),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
) {
// App Icon / Logo Platzhalter
Surface(
modifier = Modifier.size(40.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primary
) {
Icon(
imageVector = Icons.Default.Adjust,
contentDescription = "Logo",
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.padding(Dimens.SpacingS)
)
}
Spacer(Modifier.height(Dimens.SpacingL))
// Navigations-Items
NavRailItem(
icon = Icons.Default.Dashboard,
label = "Admin",
selected = currentScreen is AppScreen.VeranstaltungVerwaltung || currentScreen is AppScreen.VeranstaltungDetail,
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) }
)
NavRailItem(
icon = Icons.Default.People,
label = "Vereine",
selected = currentScreen is AppScreen.VereinVerwaltung,
onClick = { onNavigate(AppScreen.VereinVerwaltung) }
)
NavRailItem(
icon = Icons.Default.Settings,
label = "Tools",
selected = currentScreen is AppScreen.Ping,
onClick = { onNavigate(AppScreen.Ping) }
)
}
}
}
@Composable
private fun DesktopTopBar(
private fun NavRailItem(
icon: ImageVector,
label: String,
selected: Boolean,
onClick: () -> Unit
) {
val tint = if (selected) MaterialTheme.colorScheme.primary else AppColors.NavigationContent
val background = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) else Color.Transparent
Surface(
modifier = Modifier
.size(48.dp)
.clickable(onClick = onClick),
shape = MaterialTheme.shapes.medium,
color = background
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = label,
tint = tint,
modifier = Modifier.size(Dimens.IconSizeM)
)
}
}
}
/**
* TopHeader: Schlanke Leiste mit Breadcrumb und Logout.
*/
@Composable
private fun DesktopTopHeader(
currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit,
onBack: () -> Unit,
onLogout: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.background(TopBarColor)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
Surface(
modifier = Modifier.fillMaxWidth().height(Dimens.TopBarHeight),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Zurück-Pfeil: für alle außer Onboarding anzeigen (damit man von "Verwaltung" zurück kommt)
if (currentScreen !is AppScreen.Onboarding) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück",
tint = TopBarTextColor,
modifier = Modifier
.size(20.dp)
.clickable { onBack() },
)
Spacer(Modifier.width(8.dp))
Row(
modifier = Modifier.fillMaxSize().padding(horizontal = Dimens.SpacingL),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (currentScreen !is AppScreen.Onboarding) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück",
modifier = Modifier.size(Dimens.IconSizeM),
tint = MaterialTheme.colorScheme.primary
)
}
Spacer(Modifier.width(Dimens.SpacingS))
}
// Breadcrumb-Segmente
BreadcrumbContent(currentScreen, onNavigate)
}
// Root-Link
Text(
text = "Verwaltung",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
)
// Breadcrumb-Segmente je nach Screen
when (currentScreen) {
is AppScreen.VeranstalterAuswahl -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter auswählen",
color = TopBarTextColor,
fontSize = 14.sp,
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
// Profil / Logout Bereich
Text(
text = "Administrator",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
IconButton(onClick = onLogout) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout,
contentDescription = "Abmelden",
modifier = Modifier.size(Dimens.IconSizeM),
tint = MaterialTheme.colorScheme.error
)
}
is AppScreen.VeranstalterNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Neuer Veranstalter",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.VeranstalterDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Veranstalter #${currentScreen.veranstalterId}",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.VeranstaltungProfil -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Veranstalter #${currentScreen.veranstalterId}",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
},
)
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.VeranstaltungDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.id}",
color = TopBarTextColor,
fontSize = 14.sp,
)
}
is AppScreen.VeranstaltungNeu -> {
BreadcrumbSeparator()
Text(
text = "Neue Veranstaltung",
color = TopBarTextColor,
fontSize = 14.sp,
)
}
is AppScreen.TurnierDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
Text(
text = "Turnier ${currentScreen.turnierId}",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.Billing -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungProfil(0, currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
Text(
text = "Turnier ${currentScreen.turnierId}",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable {
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
},
)
BreadcrumbSeparator()
Text(
text = "Abrechnung",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.TurnierNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp,
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
Text(
text = "Neues Turnier",
color = TopBarTextColor,
fontSize = 14.sp,
)
}
is AppScreen.Ping -> {
BreadcrumbSeparator()
Text(
text = "Ping Service",
color = TopBarTextColor,
fontSize = 14.sp,
)
}
is AppScreen.Vereine -> {
BreadcrumbSeparator()
Text(
text = "Vereine",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.Meisterschaften -> {
BreadcrumbSeparator()
Text(
text = "Meisterschaften",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
is AppScreen.Cups -> {
BreadcrumbSeparator()
Text(
text = "Cups",
color = TopBarTextColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
)
}
else -> {}
}
}
}
}
// Logout wurde auf Kundenwunsch entfernt
@Composable
private fun BreadcrumbContent(
currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit
) {
when (currentScreen) {
is AppScreen.VeranstalterAuswahl -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter auswählen",
style = MaterialTheme.typography.bodyMedium,
)
}
is AppScreen.VeranstalterNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Neuer Veranstalter",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.VeranstalterDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Veranstalter #${currentScreen.veranstalterId}",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.VeranstaltungProfil -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
)
BreadcrumbSeparator()
Text(
text = "Veranstalter #${currentScreen.veranstalterId}",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
},
)
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.VeranstaltungDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.id}",
style = MaterialTheme.typography.bodyMedium,
)
}
is AppScreen.VeranstaltungNeu -> {
BreadcrumbSeparator()
Text(
text = "Neue Veranstaltung",
style = MaterialTheme.typography.bodyMedium,
)
}
is AppScreen.TurnierDetail -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
Text(
text = "Turnier ${currentScreen.turnierId}",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.Billing -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungProfil(0, currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
Text(
text = "Turnier ${currentScreen.turnierId}",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable {
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
},
)
BreadcrumbSeparator()
Text(
text = "Abrechnung",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.TurnierNeu -> {
BreadcrumbSeparator()
Text(
text = "Veranstaltung #${currentScreen.veranstaltungId}",
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
modifier = Modifier.clickable {
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
Text(
text = "Neues Turnier",
style = MaterialTheme.typography.bodyMedium,
)
}
is AppScreen.Ping -> {
BreadcrumbSeparator()
Text(
text = "Ping Service",
style = MaterialTheme.typography.bodyMedium,
)
}
is AppScreen.Vereine -> {
BreadcrumbSeparator()
Text(
text = "Vereine",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.Meisterschaften -> {
BreadcrumbSeparator()
Text(
text = "Meisterschaften",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
is AppScreen.Cups -> {
BreadcrumbSeparator()
Text(
text = "Cups",
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
else -> {}
}
}
@@ -733,45 +807,71 @@ private fun DesktopContentArea(
@Composable
private fun DesktopFooterBar() {
// Stub-Status für MVP
// Echte Status-Logik vorbereitet
val online = remember { mutableStateOf(true) }
val deviceConnected = remember { mutableStateOf(true) }
val deviceName = "Richter-Turm"
Row(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.background(Color(0xFFF3F4F6))
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
Surface(
color = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
tonalElevation = 1.dp
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = if (online.value) Icons.Filled.Wifi else Icons.Filled.WifiOff,
contentDescription = null,
tint = if (online.value) Color(0xFF059669) else Color(0xFFDC2626)
)
Spacer(Modifier.width(6.dp))
Text(if (online.value) "Online" else "Offline", color = Color(0xFF374151), fontSize = 12.sp)
Spacer(Modifier.width(16.dp))
Icon(Icons.Filled.Devices, contentDescription = null, tint = if (deviceConnected.value) Color(0xFF2563EB) else Color(0xFF9CA3AF))
Spacer(Modifier.width(6.dp))
Text(
if (deviceConnected.value) "Verbunden: $deviceName" else "Kein Gerät verbunden",
color = Color(0xFF374151),
fontSize = 12.sp
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (deviceConnected.value) {
OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) {
Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
Spacer(Modifier.width(6.dp))
Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp)
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.padding(horizontal = Dimens.SpacingS),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Status: Cloud Sync
StatusIndicator(
icon = if (online.value) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
label = if (online.value) "Cloud synchronisiert" else "Offline (Lokal)",
color = if (online.value) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
)
Spacer(Modifier.width(Dimens.SpacingM))
// Status: LAN Devices (mDNS)
StatusIndicator(
icon = Icons.Filled.Lan,
label = if (deviceConnected.value) "Verbunden: $deviceName" else "Suche nach Geräten...",
color = if (deviceConnected.value) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "v2.4.0-rc1 | Desktop-Alpha",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
private fun StatusIndicator(
icon: ImageVector,
label: String,
color: Color
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(Dimens.IconSizeS)
)
Spacer(Modifier.width(Dimens.SpacingXS))
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface
)
}
}
@@ -1,5 +1,6 @@
package at.mocode.desktop.v2
import at.mocode.frontend.core.designsystem.components.MsTextField
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -51,10 +52,10 @@ fun OnboardingScreen(
val frKey = remember { FocusRequester() }
val frBtn = remember { FocusRequester() }
OutlinedTextField(
MsTextField(
value = geraetName,
onValueChange = { onGeraetNameChange(it) },
label = { Text("Gerätename (Pflicht)") },
label = "Gerätename (Pflicht)",
modifier = Modifier
.fillMaxWidth()
.focusRequester(frName)
@@ -70,18 +71,15 @@ fun OnboardingScreen(
} else false
}
,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
imeAction = ImeAction.Next,
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
)
OutlinedTextField(
MsTextField(
value = secureKey,
onValueChange = { onSecureKeyChange(it) },
label = { Text("Sicherheitsschlüssel (Pflicht)") },
trailingIcon = {
IconButton(onClick = { showPw = !showPw }) {
Icon(if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility, contentDescription = null)
}
},
label = "Sicherheitsschlüssel (Pflicht)",
trailingIcon = if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility,
onTrailingIconClick = { showPw = !showPw },
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
modifier = Modifier
.fillMaxWidth()
@@ -106,7 +104,7 @@ fun OnboardingScreen(
} else false
}
,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
imeAction = ImeAction.Done,
keyboardActions = KeyboardActions(onDone = {
if (geraetName.trim().length >= 3 && secureKey.trim().length >= 8) {
onContinue(geraetName, secureKey)
@@ -189,14 +187,14 @@ fun PferdProfilV2(id: Long, onBack: () -> Unit) {
title = { Text("Pferd bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
MsTextField(name, { name = it }, label = "Name", modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(geb, { geb = it }, label = { Text("Geburtsdatum") }, modifier = Modifier.weight(1f))
OutlinedTextField(farbe, { farbe = it }, label = { Text("Farbe") }, modifier = Modifier.weight(1f))
MsTextField(geb, { geb = it }, label = "Geburtsdatum", modifier = Modifier.weight(1f))
MsTextField(farbe, { farbe = it }, label = "Farbe", modifier = Modifier.weight(1f))
}
}
}
@@ -261,16 +259,16 @@ fun ReiterProfilV2(id: Long, onBack: () -> Unit) {
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(vor, { vor = it }, label = { Text("Vorname") }, modifier = Modifier.weight(1f))
OutlinedTextField(nach, { nach = it }, label = { Text("Nachname") }, modifier = Modifier.weight(1f))
MsTextField(vor, { vor = it }, label = "Vorname", modifier = Modifier.weight(1f))
MsTextField(nach, { nach = it }, label = "Nachname", modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(liz, { liz = it }, label = { Text("Lizenzklasse") }, modifier = Modifier.weight(1f))
OutlinedTextField(verein, { verein = it }, label = { Text("Verein") }, modifier = Modifier.weight(1f))
MsTextField(liz, { liz = it }, label = "Lizenzklasse", modifier = Modifier.weight(1f))
MsTextField(verein, { verein = it }, label = "Verein", modifier = Modifier.weight(1f))
}
}
}