chore: erweitere Pferd-, Funktionär- und Reiter-Modelle um neue Felder, verbessere UI und Suche

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-21 13:47:35 +02:00
parent 574f8c470c
commit 2662d4e82e
10 changed files with 399 additions and 196 deletions

View File

@ -47,3 +47,17 @@ Die Pferdesportliche Logik (§ 39) ist nun im Wizard sichtbar. Der nächste Schr
- Korrektur der `LaunchedEffect`-Logik in `DesktopMainLayout.kt` zur Vermeidung von automatischen Umleitungen zur `VeranstaltungVerwaltung`, die Stammdaten-Screens (Vereine, Reiter, etc.) blockierten. - Korrektur der `LaunchedEffect`-Logik in `DesktopMainLayout.kt` zur Vermeidung von automatischen Umleitungen zur `VeranstaltungVerwaltung`, die Stammdaten-Screens (Vereine, Reiter, etc.) blockierten.
- Erweiterung des Login-Gates in `DesktopApp.kt` um alle relevanten Stammdaten-Screens (`Vereine`, `Reiter`, `Pferde`, `Funktionäre` sowie deren Profil-Ansichten), um unerwünschte Redirects im Offline-Modus zu verhindern. - Erweiterung des Login-Gates in `DesktopApp.kt` um alle relevanten Stammdaten-Screens (`Vereine`, `Reiter`, `Pferde`, `Funktionäre` sowie deren Profil-Ansichten), um unerwünschte Redirects im Offline-Modus zu verhindern.
- Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls. - Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls.
### 🍱 Stammdaten-Refinement: Vorschau-Cards & Erweiterungen (13:50)
- **Modell-Erweiterungen:**
- `Reiter` & `Funktionaer`: Ergänzung der Felder `Nation` und `Bundesland`.
- `Pferd`: Einführung der `Kopfnummer`.
- `Funktionaer`: Konsolidierung der `Qualifikation`.
- **UI-Modernisierung:**
- Implementierung von `ReiterCardPreview`, `PferdCardPreview` und `FunktionaerCardPreview` zur schnellen Identifikation in der Detailansicht.
- Integration dieser Vorschau-Cards in die jeweiligen Screens (`Master-Detail-Layout`) sowie als Orientierungshilfe in den Editoren.
- Anpassung der Pferde-Liste um die Spalte `Kopf-Nr.`.
- **Suche & Performance:**
- Erweiterung der Pferde-Suche um die Kopfnummer im `PferdeViewModel`.
- Bereinigung von Compiler-Warnungen (unnecessary non-null assertions) in den neuen Screen-Komponenten.
- Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls (`BUILD SUCCESSFUL`).

View File

@ -11,5 +11,8 @@ data class Funktionaer(
val email: String? = null, val email: String? = null,
val telefon: String? = null, val telefon: String? = null,
val vereinsNummer: String? = null, val vereinsNummer: String? = null,
val nation: String? = "AUT",
val bundesland: String? = null,
val qualifikation: String? = null,
val istAktiv: Boolean = true val istAktiv: Boolean = true
) )

View File

@ -1,15 +1,19 @@
package at.mocode.frontend.features.funktionaer.presentation package at.mocode.frontend.features.funktionaer.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Gavel import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.* import at.mocode.frontend.core.designsystem.components.*
@ -40,14 +44,21 @@ fun FunktionaerScreen(
onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) }, onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) },
onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) }, onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) },
onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) }, onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) },
onNationChange = { viewModel.send(FunktionaerIntent.EditNation(it)) },
onBundeslandChange = { viewModel.send(FunktionaerIntent.EditBundesland(it)) },
onQualifikationChange = { viewModel.send(FunktionaerIntent.EditQualifikation(it)) },
onSave = { viewModel.send(FunktionaerIntent.Save) }, onSave = { viewModel.send(FunktionaerIntent.Save) },
onCancel = { viewModel.send(FunktionaerIntent.Cancel) } onCancel = { viewModel.send(FunktionaerIntent.Cancel) }
) )
} else if (state.selectedFunktionaer != null) { } else if (state.selectedFunktionaer != null) {
Column(Modifier.fillMaxSize()) {
FunktionaerCardPreview(funktionaer = state.selectedFunktionaer!!)
Spacer(Modifier.height(16.dp))
FunktionaerCard( FunktionaerCard(
funktionaer = state.selectedFunktionaer!!, funktionaer = state.selectedFunktionaer!!,
onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) } onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) }
) )
}
} else { } else {
PlaceholderContent( PlaceholderContent(
title = "Kein Funktionär ausgewählt", title = "Kein Funktionär ausgewählt",
@ -123,61 +134,26 @@ fun FunktionaerCard(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().wrapContentHeight(), modifier = Modifier.fillMaxWidth().wrapContentHeight()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Row( Row(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(), FunktionaerDetailItem(label = "Richter-Nr.", value = funktionaer.richterNummer ?: "-", modifier = Modifier.weight(1f))
horizontalArrangement = Arrangement.SpaceBetween, FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f))
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
modifier = Modifier.size(48.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
Icons.Default.Gavel,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(Modifier.width(16.dp))
Column {
Text(
"${funktionaer.vorname} ${funktionaer.nachname}",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Text(
"Richter-Nr: ${funktionaer.richterNummer ?: "-"}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
MsStatusBadge( Spacer(Modifier.height(16.dp))
text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv",
containerColor = (if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error).copy(alpha = 0.1f),
contentColor = if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
)
}
Spacer(Modifier.height(24.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(Modifier.height(24.dp))
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f)) FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.qualifikation ?: "-", modifier = Modifier.weight(1f))
FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.richterQualifikation ?: "-", modifier = Modifier.weight(1f)) FunktionaerDetailItem(label = "Sparte(n)", value = funktionaer.qualifiziertFuerSparten.joinToString(", ").ifBlank { "-" }, modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
FunktionaerDetailItem(label = "Nation", value = funktionaer.nation ?: "-", modifier = Modifier.weight(1f))
FunktionaerDetailItem(label = "Bundesland", value = funktionaer.bundesland ?: "-", modifier = Modifier.weight(1f))
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@ -199,6 +175,50 @@ fun FunktionaerCard(
} }
} }
@Composable
fun FunktionaerCardPreview(funktionaer: Funktionaer) {
Card(
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Person, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("${funktionaer.vorname} ${funktionaer.nachname}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
MsStatusBadge(
text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv",
containerColor = (if (funktionaer.istAktiv) Color(0xFF2E7D32) else Color.Gray).copy(alpha = 0.1f),
contentColor = if (funktionaer.istAktiv) Color(0xFF2E7D32) else Color.Gray
)
}
Text(
text = buildString {
append("Nr: ${funktionaer.richterNummer ?: "-"}")
if (!funktionaer.nation.isNullOrBlank()) append(" | ${funktionaer.nation}")
if (!funktionaer.bundesland.isNullOrBlank()) append(" (${funktionaer.bundesland})")
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable @Composable
private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) { private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) { Column(modifier = modifier) {
@ -215,17 +235,26 @@ private fun FunktionaerEditorContent(
onRichterNummerChange: (String) -> Unit, onRichterNummerChange: (String) -> Unit,
onEmailChange: (String) -> Unit, onEmailChange: (String) -> Unit,
onTelefonChange: (String) -> Unit, onTelefonChange: (String) -> Unit,
onNationChange: (String) -> Unit,
onBundeslandChange: (String) -> Unit,
onQualifikationChange: (String) -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MsActionToolbar( MsActionToolbar(
title = "Funktionär Details", title = if (state.selectedFunktionaer == null) "Funktionär anlegen" else "Funktionär Details",
onSave = onSave, onSave = onSave,
onCancel = onCancel onCancel = onCancel
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(16.dp))
// Preview in Editor
if (state.selectedFunktionaer != null) {
FunktionaerCardPreview(funktionaer = state.selectedFunktionaer)
Spacer(Modifier.height(16.dp))
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField( MsTextField(
@ -246,13 +275,41 @@ private fun FunktionaerEditorContent(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField( MsTextField(
value = state.editRichterNummer, value = state.editRichterNummer,
onValueChange = onRichterNummerChange, onValueChange = onRichterNummerChange,
label = "Richter-Nummer", label = "Richter-Nummer",
modifier = Modifier.width(300.dp), modifier = Modifier.weight(1f),
compact = true compact = true
) )
MsTextField(
value = state.editQualifikation,
onValueChange = onQualifikationChange,
label = "Qualifikation",
modifier = Modifier.weight(1f),
compact = true
)
}
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField(
value = state.editNation,
onValueChange = onNationChange,
label = "Nation",
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = state.editBundesland,
onValueChange = onBundeslandChange,
label = "Bundesland",
modifier = Modifier.weight(1f),
compact = true
)
}
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))

View File

@ -1,8 +1,8 @@
package at.mocode.frontend.features.funktionaer.presentation package at.mocode.frontend.features.funktionaer.presentation
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -26,6 +26,9 @@ data class FunktionaerState(
val editRichterNummer: String = "", val editRichterNummer: String = "",
val editEmail: String = "", val editEmail: String = "",
val editTelefon: String = "", val editTelefon: String = "",
val editNation: String = "AUT",
val editBundesland: String = "",
val editQualifikation: String = "",
val errorMessage: String? = null, val errorMessage: String? = null,
) )
@ -40,6 +43,9 @@ sealed interface FunktionaerIntent {
data class EditRichterNummer(val value: String) : FunktionaerIntent data class EditRichterNummer(val value: String) : FunktionaerIntent
data class EditEmail(val value: String) : FunktionaerIntent data class EditEmail(val value: String) : FunktionaerIntent
data class EditTelefon(val value: String) : FunktionaerIntent data class EditTelefon(val value: String) : FunktionaerIntent
data class EditNation(val value: String) : FunktionaerIntent
data class EditBundesland(val value: String) : FunktionaerIntent
data class EditQualifikation(val value: String) : FunktionaerIntent
data object Save : FunktionaerIntent data object Save : FunktionaerIntent
data object Cancel : FunktionaerIntent data object Cancel : FunktionaerIntent
data object ClearError : FunktionaerIntent data object ClearError : FunktionaerIntent
@ -69,7 +75,10 @@ class FunktionaerViewModel(
editNachname = intent.funktionaer?.nachname ?: "", editNachname = intent.funktionaer?.nachname ?: "",
editRichterNummer = intent.funktionaer?.richterNummer ?: "", editRichterNummer = intent.funktionaer?.richterNummer ?: "",
editEmail = intent.funktionaer?.email ?: "", editEmail = intent.funktionaer?.email ?: "",
editTelefon = intent.funktionaer?.telefon ?: "" editTelefon = intent.funktionaer?.telefon ?: "",
editNation = intent.funktionaer?.nation ?: "AUT",
editBundesland = intent.funktionaer?.bundesland ?: "",
editQualifikation = intent.funktionaer?.qualifikation ?: ""
) )
} }
@ -81,7 +90,10 @@ class FunktionaerViewModel(
editNachname = "", editNachname = "",
editRichterNummer = "", editRichterNummer = "",
editEmail = "", editEmail = "",
editTelefon = "" editTelefon = "",
editNation = "AUT",
editBundesland = "",
editQualifikation = ""
) )
} }
@ -90,6 +102,9 @@ class FunktionaerViewModel(
is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) } is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) }
is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) } is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) }
is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) } is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) }
is FunktionaerIntent.EditNation -> reduce { it.copy(editNation = intent.value) }
is FunktionaerIntent.EditBundesland -> reduce { it.copy(editBundesland = intent.value) }
is FunktionaerIntent.EditQualifikation -> reduce { it.copy(editQualifikation = intent.value) }
is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) } is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) }
is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) } is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) }
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) } is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }

View File

@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Color
data class Pferd( data class Pferd(
val id: String, val id: String,
val name: String, val name: String,
val kopfNummer: String? = null,
val lebensnummer: String, val lebensnummer: String,
val geschlecht: Geschlecht = Geschlecht.WALLACH, val geschlecht: Geschlecht = Geschlecht.WALLACH,
val farbe: String = "", val farbe: String = "",

View File

@ -1,6 +1,8 @@
package at.mocode.frontend.features.pferde.presentation package at.mocode.frontend.features.pferde.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Pets import androidx.compose.material.icons.filled.Pets
@ -8,6 +10,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.* import at.mocode.frontend.core.designsystem.components.*
@ -36,6 +39,7 @@ fun PferdeScreen(
PferdeEditorContent( PferdeEditorContent(
uiState = uiState, uiState = uiState,
onNameChange = viewModel::onEditNameChange, onNameChange = viewModel::onEditNameChange,
onKopfNummerChange = viewModel::onEditKopfNummerChange,
onLebensnummerChange = viewModel::onEditLebensnummerChange, onLebensnummerChange = viewModel::onEditLebensnummerChange,
onGeschlechtChange = viewModel::onEditGeschlechtChange, onGeschlechtChange = viewModel::onEditGeschlechtChange,
onFarbeChange = viewModel::onEditFarbeChange, onFarbeChange = viewModel::onEditFarbeChange,
@ -48,10 +52,14 @@ fun PferdeScreen(
onCancel = viewModel::onCancel onCancel = viewModel::onCancel
) )
} else if (uiState.selectedPferd != null) { } else if (uiState.selectedPferd != null) {
Column(Modifier.fillMaxSize()) {
PferdCardPreview(pferd = uiState.selectedPferd)
Spacer(Modifier.height(16.dp))
PferdCard( PferdCard(
pferd = uiState.selectedPferd, pferd = uiState.selectedPferd,
onEdit = { viewModel.selectPferd(uiState.selectedPferd) } onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
) )
}
} else { } else {
PlaceholderContent( PlaceholderContent(
title = "Kein Pferd ausgewählt", title = "Kein Pferd ausgewählt",
@ -88,6 +96,11 @@ private fun PferdeListContent(
MsDataTable( MsDataTable(
items = uiState.searchResults, items = uiState.searchResults,
columns = listOf( columns = listOf(
MsColumnDefinition(
title = "Kopf-Nr.",
width = 80.dp,
cellRenderer = { Text(it.kopfNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition( MsColumnDefinition(
title = "Name", title = "Name",
weight = 1f, weight = 1f,
@ -127,61 +140,12 @@ fun PferdCard(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().wrapContentHeight(), modifier = Modifier.fillMaxWidth().wrapContentHeight()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
modifier = Modifier.size(48.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
Icons.Default.Pets,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(Modifier.width(16.dp))
Column {
Text(
pferd.name,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Text(
pferd.lebensnummer,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
MsStatusBadge(
text = pferd.status.label,
containerColor = pferd.status.color.copy(alpha = 0.1f),
contentColor = pferd.status.color
)
}
Spacer(Modifier.height(24.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(Modifier.height(24.dp))
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f)) DetailItem(label = "Kopf-Nummer", value = pferd.kopfNummer ?: "-", modifier = Modifier.weight(1f))
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f)) DetailItem(label = "Lebensnummer", value = pferd.lebensnummer, modifier = Modifier.weight(1f))
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@ -195,15 +159,62 @@ fun PferdCard(
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f)) DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f))
DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f)) DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f))
} }
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
MsButton( MsButton(
onClick = onEdit,
text = "Pferdedaten bearbeiten", text = "Pferdedaten bearbeiten",
modifier = Modifier.fillMaxWidth() onClick = onEdit,
fullWidth = true
)
}
}
}
}
@Composable
fun PferdCardPreview(pferd: Pferd) {
Card(
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Pets, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(pferd.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
MsStatusBadge(
text = pferd.status.label,
containerColor = pferd.status.color.copy(alpha = 0.1f),
contentColor = pferd.status.color
)
}
Text(
text = "Kopf-Nr: ${pferd.kopfNummer ?: "-"} | Lebensnummer: ${pferd.lebensnummer}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@ -222,6 +233,7 @@ private fun DetailItem(label: String, value: String, modifier: Modifier = Modifi
private fun PferdeEditorContent( private fun PferdeEditorContent(
uiState: PferdeUiState, uiState: PferdeUiState,
onNameChange: (String) -> Unit, onNameChange: (String) -> Unit,
onKopfNummerChange: (String) -> Unit,
onLebensnummerChange: (String) -> Unit, onLebensnummerChange: (String) -> Unit,
onGeschlechtChange: (Geschlecht) -> Unit, onGeschlechtChange: (Geschlecht) -> Unit,
onFarbeChange: (String) -> Unit, onFarbeChange: (String) -> Unit,
@ -233,23 +245,41 @@ private fun PferdeEditorContent(
onSave: () -> Unit, onSave: () -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MsActionToolbar( MsActionToolbar(
title = "Pferde Details", title = if (uiState.selectedPferd == null) "Pferd anlegen" else "Pferde Details",
onSave = onSave, onSave = onSave,
onCancel = onCancel onCancel = onCancel
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(16.dp))
// Preview in Editor
if (uiState.selectedPferd != null) {
PferdCardPreview(pferd = uiState.selectedPferd)
Spacer(Modifier.height(16.dp))
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField( MsTextField(
value = uiState.editName, value = uiState.editName,
onValueChange = onNameChange, onValueChange = onNameChange,
label = "Name", label = "Pferdename",
modifier = Modifier.weight(1.5f),
compact = true
)
MsTextField(
value = uiState.editKopfNummer,
onValueChange = onKopfNummerChange,
label = "Kopf-Nummer",
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
compact = true compact = true
) )
}
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField( MsTextField(
value = uiState.editLebensnummer, value = uiState.editLebensnummer,
onValueChange = onLebensnummerChange, onValueChange = onLebensnummerChange,
@ -257,6 +287,13 @@ private fun PferdeEditorContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
compact = true compact = true
) )
MsTextField(
value = uiState.editOepsNummer,
onValueChange = onOepsNummerChange,
label = "ÖPS Nummer",
modifier = Modifier.weight(1f),
compact = true
)
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))

View File

@ -26,7 +26,8 @@ data class PferdeUiState(
val editStatus: PferdeStatus = PferdeStatus.AKTIV, val editStatus: PferdeStatus = PferdeStatus.AKTIV,
val editFeiId: String = "", val editFeiId: String = "",
val editOepsNummer: String = "", val editOepsNummer: String = "",
val editBesitzer: String = "" val editBesitzer: String = "",
val editKopfNummer: String = ""
) )
/** /**
@ -44,16 +45,33 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
private fun loadPferde() { private fun loadPferde() {
val mockData = listOf( val mockData = listOf(
Pferd("1", "Bella", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV), Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
Pferd("2", "Casanova", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV), Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
Pferd("3", "Spirit", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV), Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
Pferd("4", "Lucky", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT) Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
) )
uiState = uiState.copy(searchResults = mockData) uiState = uiState.copy(searchResults = mockData)
} }
fun onSearchQueryChange(query: String) { fun onSearchQueryChange(query: String) {
uiState = uiState.copy(searchQuery = query) uiState = uiState.copy(searchQuery = query)
val allPferde = listOf(
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
)
val filtered = if (query.isBlank()) {
allPferde
} else {
allPferde.filter {
it.name.contains(query, ignoreCase = true) ||
it.lebensnummer.contains(query, ignoreCase = true) ||
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
}
}
uiState = uiState.copy(searchResults = filtered)
} }
fun selectPferd(pferd: Pferd) { fun selectPferd(pferd: Pferd) {
@ -69,7 +87,8 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
editStatus = pferd.status, editStatus = pferd.status,
editFeiId = pferd.feiId ?: "", editFeiId = pferd.feiId ?: "",
editOepsNummer = pferd.oepsNummer ?: "", editOepsNummer = pferd.oepsNummer ?: "",
editBesitzer = pferd.besitzer ?: "" editBesitzer = pferd.besitzer ?: "",
editKopfNummer = pferd.kopfNummer ?: ""
) )
} }
@ -86,7 +105,8 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
editStatus = PferdeStatus.AKTIV, editStatus = PferdeStatus.AKTIV,
editFeiId = "", editFeiId = "",
editOepsNummer = "", editOepsNummer = "",
editBesitzer = "" editBesitzer = "",
editKopfNummer = ""
) )
} }
@ -102,6 +122,10 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
uiState = uiState.copy(editBesitzer = value) uiState = uiState.copy(editBesitzer = value)
} }
fun onEditKopfNummerChange(value: String) {
uiState = uiState.copy(editKopfNummer = value)
}
fun onEditNameChange(value: String) { fun onEditNameChange(value: String) {
uiState = uiState.copy(editName = value) uiState = uiState.copy(editName = value)
} }

View File

@ -18,7 +18,9 @@ data class Reiter(
val geburtsdatum: String? = null, val geburtsdatum: String? = null,
val email: String? = null, val email: String? = null,
val telefon: String? = null, val telefon: String? = null,
val verein: String? = null val verein: String? = null,
val nation: String? = "AUT",
val bundesland: String? = null
) { ) {
val name: String get() = "$vorname $nachname" val name: String get() = "$vorname $nachname"
} }

View File

@ -1,10 +1,16 @@
package at.mocode.frontend.features.reiter.presentation package at.mocode.frontend.features.reiter.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.* import at.mocode.frontend.core.designsystem.components.*
import at.mocode.frontend.core.designsystem.models.PlaceholderContent import at.mocode.frontend.core.designsystem.models.PlaceholderContent
@ -41,14 +47,20 @@ fun ReiterScreen(
onEmailChange = viewModel::onEditEmailChange, onEmailChange = viewModel::onEditEmailChange,
onTelefonChange = viewModel::onEditTelefonChange, onTelefonChange = viewModel::onEditTelefonChange,
onVereinChange = viewModel::onEditVereinChange, onVereinChange = viewModel::onEditVereinChange,
onNationChange = viewModel::onEditNationChange,
onBundeslandChange = viewModel::onEditBundeslandChange,
onSave = viewModel::onSave, onSave = viewModel::onSave,
onCancel = viewModel::onCancel onCancel = viewModel::onCancel
) )
} else if (uiState.selectedReiter != null) { } else if (uiState.selectedReiter != null) {
Column(Modifier.fillMaxSize()) {
ReiterCardPreview(reiter = uiState.selectedReiter)
Spacer(Modifier.height(16.dp))
ReiterCard( ReiterCard(
reiter = uiState.selectedReiter, reiter = uiState.selectedReiter,
onEdit = { viewModel.selectReiter(uiState.selectedReiter) } onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
) )
}
} else { } else {
PlaceholderContent( PlaceholderContent(
title = "Kein Reiter ausgewählt", title = "Kein Reiter ausgewählt",
@ -122,57 +134,9 @@ fun ReiterCard(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().wrapContentHeight(), modifier = Modifier.fillMaxWidth().wrapContentHeight()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
modifier = Modifier.size(48.dp),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = (reiter.vorname.take(1) + reiter.nachname.take(1)).uppercase(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
Spacer(Modifier.width(16.dp))
Column {
Text(
reiter.name,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
Text(
"ÖPS-Nr: ${reiter.oepsNummer ?: "-"}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
MsStatusBadge(
text = reiter.status.label,
containerColor = reiter.status.color.copy(alpha = 0.1f),
contentColor = reiter.status.color
)
}
Spacer(Modifier.height(24.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(Modifier.height(24.dp))
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f)) ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f))
ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f)) ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f))
@ -192,6 +156,13 @@ fun ReiterCard(
ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f)) ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f))
} }
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
ReiterDetailItem(label = "Nation", value = reiter.nation ?: "-", modifier = Modifier.weight(1f))
ReiterDetailItem(label = "Bundesland", value = reiter.bundesland ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))
MsButton( MsButton(
@ -204,6 +175,50 @@ fun ReiterCard(
} }
} }
@Composable
fun ReiterCardPreview(reiter: Reiter) {
Card(
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Person, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(reiter.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
MsStatusBadge(
text = reiter.status.label,
containerColor = reiter.status.color.copy(alpha = 0.1f),
contentColor = reiter.status.color
)
}
Text(
text = buildString {
append("ÖPS: ${reiter.oepsNummer ?: "-"}")
if (!reiter.nation.isNullOrBlank()) append(" | ${reiter.nation}")
if (!reiter.bundesland.isNullOrBlank()) append(" (${reiter.bundesland})")
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable @Composable
private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) { private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) { Column(modifier = modifier) {
@ -225,17 +240,25 @@ private fun ReiterEditorContent(
onEmailChange: (String) -> Unit, onEmailChange: (String) -> Unit,
onTelefonChange: (String) -> Unit, onTelefonChange: (String) -> Unit,
onVereinChange: (String) -> Unit, onVereinChange: (String) -> Unit,
onNationChange: (String) -> Unit,
onBundeslandChange: (String) -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MsActionToolbar( MsActionToolbar(
title = "Reiter Details", title = if (uiState.selectedReiter == null) "Reiter anlegen" else "Reiter Details",
onSave = onSave, onSave = onSave,
onCancel = onCancel onCancel = onCancel
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(16.dp))
// Preview in Editor
if (uiState.selectedReiter != null) {
ReiterCardPreview(reiter = uiState.selectedReiter)
Spacer(Modifier.height(16.dp))
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField( MsTextField(
@ -294,6 +317,25 @@ private fun ReiterEditorContent(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField(
value = uiState.editNation,
onValueChange = onNationChange,
label = "Nation",
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editBundesland,
onValueChange = onBundeslandChange,
label = "Bundesland",
modifier = Modifier.weight(1f),
compact = true
)
}
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField( MsTextField(
value = uiState.editEmail, value = uiState.editEmail,

View File

@ -29,7 +29,9 @@ data class ReiterUiState(
val editGeburtsdatum: String = "", val editGeburtsdatum: String = "",
val editEmail: String = "", val editEmail: String = "",
val editTelefon: String = "", val editTelefon: String = "",
val editVerein: String = "" val editVerein: String = "",
val editNation: String = "AUT",
val editBundesland: String = ""
) )
/** /**
@ -77,7 +79,9 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
editGeburtsdatum = reiter.geburtsdatum ?: "", editGeburtsdatum = reiter.geburtsdatum ?: "",
editEmail = reiter.email ?: "", editEmail = reiter.email ?: "",
editTelefon = reiter.telefon ?: "", editTelefon = reiter.telefon ?: "",
editVerein = reiter.verein ?: "" editVerein = reiter.verein ?: "",
editNation = reiter.nation ?: "AUT",
editBundesland = reiter.bundesland ?: ""
) )
} }
@ -96,7 +100,9 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
editGeburtsdatum = "", editGeburtsdatum = "",
editEmail = "", editEmail = "",
editTelefon = "", editTelefon = "",
editVerein = "" editVerein = "",
editNation = "AUT",
editBundesland = ""
) )
} }
@ -106,6 +112,8 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
fun onEditEmailChange(value: String) { uiState = uiState.copy(editEmail = value) } fun onEditEmailChange(value: String) { uiState = uiState.copy(editEmail = value) }
fun onEditTelefonChange(value: String) { uiState = uiState.copy(editTelefon = value) } fun onEditTelefonChange(value: String) { uiState = uiState.copy(editTelefon = value) }
fun onEditVereinChange(value: String) { uiState = uiState.copy(editVerein = value) } fun onEditVereinChange(value: String) { uiState = uiState.copy(editVerein = value) }
fun onEditNationChange(value: String) { uiState = uiState.copy(editNation = value) }
fun onEditBundeslandChange(value: String) { uiState = uiState.copy(editBundesland = value) }
fun onEditVornameChange(value: String) { fun onEditVornameChange(value: String) {
uiState = uiState.copy(editVorname = value) uiState = uiState.copy(editVorname = value)