chore: enhance Stammdaten-Verwaltung and refine desktop UX across multiple features, fix typo in settings.json, enable WASM builds, and add Master-Detail layout for Funktionäre
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled

This commit is contained in:
2026-04-20 02:49:30 +02:00
parent d4aeba4666
commit 345c329350
14 changed files with 748 additions and 123 deletions
@@ -1,5 +1,6 @@
package at.mocode.frontend.features.funktionaer.di
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import at.mocode.frontend.features.funktionaer.presentation.*
import org.koin.dsl.module
@@ -9,9 +10,9 @@ val funktionaerModule = module {
}
class MockFunktionaerRepository : FunktionaerRepository {
override suspend fun list(): List<FunktionaerListItem> = listOf(
FunktionaerListItem(1, "Wolfgang Schier", "RICHTER", "G3"),
FunktionaerListItem(2, "Alice Schwab", "RICHTER", "INTERNATIONAL"),
FunktionaerListItem(3, "Dietmar Gstöttner", "PARCOURSBAUER", null)
override suspend fun list(): List<Funktionaer> = listOf(
Funktionaer(1, "Wolfgang", "Schier", "12345", listOf("RICHTER"), "G3"),
Funktionaer(2, "Alice", "Schwab", "23456", listOf("RICHTER"), "INTERNATIONAL"),
Funktionaer(3, "Dietmar", "Gstöttner", "34567", listOf("PARCOURSBAUER"), null)
)
}
@@ -1,17 +1,20 @@
package at.mocode.frontend.features.funktionaer.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Gavel
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.*
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
@Composable
fun FunktionaerScreen(
@@ -24,19 +27,31 @@ fun FunktionaerScreen(
FunktionaerListContent(
state = state,
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) }
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) },
onAddNew = { viewModel.send(FunktionaerIntent.AddNew) }
)
},
detail = {
if (state.selectedId != null) {
val selected = state.list.find { it.id == state.selectedId }
if (selected != null) {
FunktionaerDetailContent(selected)
}
if (state.isEditing) {
FunktionaerEditorContent(
state = state,
onVornameChange = { viewModel.send(FunktionaerIntent.EditVorname(it)) },
onNachnameChange = { viewModel.send(FunktionaerIntent.EditNachname(it)) },
onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) },
onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) },
onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) },
onSave = { viewModel.send(FunktionaerIntent.Save) },
onCancel = { viewModel.send(FunktionaerIntent.Cancel) }
)
} else if (state.selectedFunktionaer != null) {
FunktionaerCard(
funktionaer = state.selectedFunktionaer!!,
onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) }
)
} else {
PlaceholderContent(
title = "Kein Funktionär ausgewählt",
subtitle = "Wählen Sie einen Funktionär aus der Liste aus."
subtitle = "Wählen Sie einen Richter oder Funktionär aus der Liste aus."
)
}
}
@@ -47,13 +62,21 @@ fun FunktionaerScreen(
private fun FunktionaerListContent(
state: FunktionaerState,
onSearchChange: (String) -> Unit,
onFunktionaerSelected: (Long) -> Unit
onFunktionaerSelected: (Funktionaer) -> Unit,
onAddNew: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
MsFilterBar(
searchQuery = state.searchQuery,
onSearchQueryChange = onSearchChange,
resultCount = state.filtered.size
resultCount = state.filtered.size,
actions = {
MsButton(
text = "Funktionär anlegen",
onClick = onAddNew,
icon = Icons.Default.Add
)
}
)
Spacer(Modifier.height(8.dp))
@@ -68,36 +91,189 @@ private fun FunktionaerListContent(
columns = listOf(
MsColumnDefinition(
title = "Name",
weight = 1.5f,
cellRenderer = { Text("${it.vorname} ${it.nachname}", style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Nr.",
width = 80.dp,
cellRenderer = { Text(it.richterNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Rollen",
weight = 1f,
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Rolle",
width = 150.dp,
cellRenderer = { Text(it.rolle, style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Lizenz",
width = 100.dp,
cellRenderer = { Text(it.lizenz ?: "-", style = MaterialTheme.typography.bodySmall) }
cellRenderer = { Text(it.rollen.joinToString(", "), style = MaterialTheme.typography.bodySmall) }
)
),
onRowClick = { onFunktionaerSelected(it.id) }
onRowClick = onFunktionaerSelected,
selectedItem = state.selectedFunktionaer
)
}
}
}
@Composable
private fun FunktionaerDetailContent(item: FunktionaerListItem) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(item.name, style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(8.dp))
Text("Rolle: ${item.rolle}", style = MaterialTheme.typography.bodyLarge)
item.lizenz?.let {
Text("Lizenz: $it", style = MaterialTheme.typography.bodyLarge)
fun FunktionaerCard(
funktionaer: Funktionaer,
onEdit: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
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.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(
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()) {
FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f))
FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.richterQualifikation ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
FunktionaerDetailItem(label = "E-Mail", value = funktionaer.email ?: "-", modifier = Modifier.weight(1f))
FunktionaerDetailItem(label = "Telefon", value = funktionaer.telefon ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(32.dp))
MsButton(
text = "Daten bearbeiten",
onClick = onEdit,
fullWidth = true
)
}
}
Spacer(Modifier.height(24.dp))
Text("Weitere Details folgen in der nächsten Ausbaustufe.", style = MaterialTheme.typography.bodyMedium)
}
}
@Composable
private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
}
}
@Composable
private fun FunktionaerEditorContent(
state: FunktionaerState,
onVornameChange: (String) -> Unit,
onNachnameChange: (String) -> Unit,
onRichterNummerChange: (String) -> Unit,
onEmailChange: (String) -> Unit,
onTelefonChange: (String) -> Unit,
onSave: () -> Unit,
onCancel: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
MsActionToolbar(
title = "Funktionär Details",
onSave = onSave,
onCancel = onCancel
)
Spacer(Modifier.height(24.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField(
value = state.editVorname,
onValueChange = onVornameChange,
label = "Vorname",
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = state.editNachname,
onValueChange = onNachnameChange,
label = "Nachname",
modifier = Modifier.weight(1f),
compact = true
)
}
Spacer(Modifier.height(16.dp))
MsTextField(
value = state.editRichterNummer,
onValueChange = onRichterNummerChange,
label = "Richter-Nummer",
modifier = Modifier.width(300.dp),
compact = true
)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField(
value = state.editEmail,
onValueChange = onEmailChange,
label = "E-Mail",
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = state.editTelefon,
onValueChange = onTelefonChange,
label = "Telefon",
modifier = Modifier.weight(1f),
compact = true
)
}
Spacer(Modifier.height(24.dp))
Text("Zusätzliche Qualifikationen und Rollen werden über das ZNS-System synchronisiert.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
@@ -1,8 +1,8 @@
package at.mocode.frontend.features.funktionaer.presentation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -17,9 +17,15 @@ data class FunktionaerListItem(
data class FunktionaerState(
val isLoading: Boolean = false,
val searchQuery: String = "",
val list: List<FunktionaerListItem> = emptyList(),
val filtered: List<FunktionaerListItem> = emptyList(),
val selectedId: Long? = null,
val list: List<Funktionaer> = emptyList(),
val filtered: List<Funktionaer> = emptyList(),
val selectedFunktionaer: Funktionaer? = null,
val isEditing: Boolean = false,
val editVorname: String = "",
val editNachname: String = "",
val editRichterNummer: String = "",
val editEmail: String = "",
val editTelefon: String = "",
val errorMessage: String? = null,
)
@@ -27,19 +33,25 @@ sealed interface FunktionaerIntent {
data object Load : FunktionaerIntent
data object Refresh : FunktionaerIntent
data class SearchChanged(val query: String) : FunktionaerIntent
data class Select(val id: Long?) : FunktionaerIntent
data class Select(val funktionaer: Funktionaer?) : FunktionaerIntent
data object AddNew : FunktionaerIntent
data class EditVorname(val value: String) : FunktionaerIntent
data class EditNachname(val value: String) : FunktionaerIntent
data class EditRichterNummer(val value: String) : FunktionaerIntent
data class EditEmail(val value: String) : FunktionaerIntent
data class EditTelefon(val value: String) : FunktionaerIntent
data object Save : FunktionaerIntent
data object Cancel : FunktionaerIntent
data object ClearError : FunktionaerIntent
}
interface FunktionaerRepository {
suspend fun list(): List<FunktionaerListItem>
suspend fun list(): List<Funktionaer>
}
class FunktionaerViewModel(
private val repo: FunktionaerRepository,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
) : ViewModel() {
private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
val state: StateFlow<FunktionaerState> = _state
@@ -49,14 +61,44 @@ class FunktionaerViewModel(
when (intent) {
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
is FunktionaerIntent.Select -> reduce { it.copy(selectedId = intent.id) }
is FunktionaerIntent.Select -> reduce {
it.copy(
selectedFunktionaer = intent.funktionaer,
isEditing = intent.funktionaer != null,
editVorname = intent.funktionaer?.vorname ?: "",
editNachname = intent.funktionaer?.nachname ?: "",
editRichterNummer = intent.funktionaer?.richterNummer ?: "",
editEmail = intent.funktionaer?.email ?: "",
editTelefon = intent.funktionaer?.telefon ?: ""
)
}
is FunktionaerIntent.AddNew -> reduce {
it.copy(
selectedFunktionaer = null,
isEditing = true,
editVorname = "",
editNachname = "",
editRichterNummer = "",
editEmail = "",
editTelefon = ""
)
}
is FunktionaerIntent.EditVorname -> reduce { it.copy(editVorname = intent.value) }
is FunktionaerIntent.EditNachname -> reduce { it.copy(editNachname = intent.value) }
is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) }
is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) }
is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) }
is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) }
is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) }
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
}
}
private fun load() {
reduce { it.copy(isLoading = true, errorMessage = null) }
scope.launch {
viewModelScope.launch {
try {
val items = repo.list()
reduce { cur ->
@@ -75,13 +117,13 @@ class FunktionaerViewModel(
reduce { it.copy(filtered = filtered) }
}
private fun filterList(list: List<FunktionaerListItem>, query: String): List<FunktionaerListItem> {
private fun filterList(list: List<Funktionaer>, query: String): List<Funktionaer> {
if (query.isBlank()) return list
val q = query.trim()
return list.filter {
it.name.contains(q, ignoreCase = true) ||
it.rolle.contains(q, ignoreCase = true) ||
(it.lizenz?.contains(q, ignoreCase = true) ?: false)
it.vorname.contains(q, ignoreCase = true) ||
it.nachname.contains(q, ignoreCase = true) ||
(it.richterNummer?.contains(q, ignoreCase = true) ?: false)
}
}
@@ -1,11 +1,14 @@
package at.mocode.frontend.features.pferde.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Pets
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.*
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
@@ -24,7 +27,8 @@ fun PferdeScreen(
PferdeListContent(
uiState = uiState,
onSearchChange = viewModel::onSearchQueryChange,
onPferdSelected = viewModel::selectPferd
onPferdSelected = viewModel::selectPferd,
onAddNew = { viewModel.addNewPferd() }
)
},
detail = {
@@ -43,6 +47,11 @@ fun PferdeScreen(
onSave = viewModel::onSave,
onCancel = viewModel::onCancel
)
} else if (uiState.selectedPferd != null) {
PferdCard(
pferd = uiState.selectedPferd,
onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
)
} else {
PlaceholderContent(
title = "Kein Pferd ausgewählt",
@@ -57,13 +66,21 @@ fun PferdeScreen(
private fun PferdeListContent(
uiState: PferdeUiState,
onSearchChange: (String) -> Unit,
onPferdSelected: (Pferd) -> Unit
onPferdSelected: (Pferd) -> Unit,
onAddNew: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
MsFilterBar(
searchQuery = uiState.searchQuery,
onSearchQueryChange = onSearchChange,
resultCount = uiState.searchResults.size
resultCount = uiState.searchResults.size,
actions = {
MsButton(
onClick = onAddNew,
text = "Pferd anlegen",
icon = Icons.Default.Add
)
}
)
Spacer(Modifier.height(8.dp))
@@ -77,9 +94,9 @@ private fun PferdeListContent(
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Lebensnummer",
width = 150.dp,
cellRenderer = { Text(it.lebensnummer, style = MaterialTheme.typography.bodySmall) }
title = "ÖPS-Nr.",
width = 100.dp,
cellRenderer = { Text(it.oepsNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Status",
@@ -93,11 +110,114 @@ private fun PferdeListContent(
}
)
),
onRowClick = onPferdSelected
onRowClick = onPferdSelected,
selectedItem = uiState.selectedPferd
)
}
}
@Composable
fun PferdCard(
pferd: Pferd,
onEdit: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
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()) {
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
DetailItem(label = "Geschlecht", value = pferd.geschlecht.label, modifier = Modifier.weight(1f))
DetailItem(label = "Farbe", value = pferd.farbe, modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(32.dp))
MsButton(
onClick = onEdit,
text = "Pferdedaten bearbeiten",
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
@Composable
private fun DetailItem(label: String, value: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
}
}
@Composable
private fun PferdeEditorContent(
uiState: PferdeUiState,
@@ -127,13 +247,15 @@ private fun PferdeEditorContent(
value = uiState.editName,
onValueChange = onNameChange,
label = "Name",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editLebensnummer,
onValueChange = onLebensnummerChange,
label = "Lebensnummer",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -144,13 +266,15 @@ private fun PferdeEditorContent(
value = uiState.editFeiId,
onValueChange = onFeiIdChange,
label = "FEI ID",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editOepsNummer,
onValueChange = onOepsNummerChange,
label = "ÖPS Nummer",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -169,7 +293,8 @@ private fun PferdeEditorContent(
value = uiState.editFarbe,
onValueChange = onFarbeChange,
label = "Farbe",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -180,13 +305,15 @@ private fun PferdeEditorContent(
value = uiState.editGeburtsjahr,
onValueChange = onGeburtsjahrChange,
label = "Geburtsjahr",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editBesitzer,
onValueChange = onBesitzerChange,
label = "Besitzer",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -17,6 +17,7 @@ data class PferdeUiState(
val selectedPferd: Pferd? = null,
val isEditing: Boolean = false,
val isLoading: Boolean = false,
val editId: String = "",
val editName: String = "",
val editLebensnummer: String = "",
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
@@ -59,6 +60,7 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
uiState = uiState.copy(
selectedPferd = pferd,
isEditing = true,
editId = pferd.id,
editName = pferd.name,
editLebensnummer = pferd.lebensnummer,
editGeschlecht = pferd.geschlecht,
@@ -71,6 +73,23 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
)
}
fun addNewPferd() {
uiState = uiState.copy(
selectedPferd = null,
isEditing = true,
editId = "",
editName = "",
editLebensnummer = "",
editGeschlecht = Geschlecht.WALLACH,
editFarbe = "",
editGeburtsjahr = "",
editStatus = PferdeStatus.AKTIV,
editFeiId = "",
editOepsNummer = "",
editBesitzer = ""
)
}
fun onEditFeiIdChange(value: String) {
uiState = uiState.copy(editFeiId = value)
}
@@ -1,9 +1,9 @@
package at.mocode.frontend.features.reiter.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.*
@@ -23,7 +23,8 @@ fun ReiterScreen(
ReiterListContent(
uiState = uiState,
onSearchChange = viewModel::onSearchQueryChange,
onReiterSelected = viewModel::selectReiter
onReiterSelected = viewModel::selectReiter,
onAddNew = { viewModel.addNewReiter() }
)
},
detail = {
@@ -43,6 +44,11 @@ fun ReiterScreen(
onSave = viewModel::onSave,
onCancel = viewModel::onCancel
)
} else if (uiState.selectedReiter != null) {
ReiterCard(
reiter = uiState.selectedReiter,
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
)
} else {
PlaceholderContent(
title = "Kein Reiter ausgewählt",
@@ -57,13 +63,20 @@ fun ReiterScreen(
private fun ReiterListContent(
uiState: ReiterUiState,
onSearchChange: (String) -> Unit,
onReiterSelected: (Reiter) -> Unit
onReiterSelected: (Reiter) -> Unit,
onAddNew: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
MsFilterBar(
searchQuery = uiState.searchQuery,
onSearchQueryChange = onSearchChange,
resultCount = uiState.searchResults.size
resultCount = uiState.searchResults.size,
actions = {
MsButton(
text = "Reiter anlegen",
onClick = onAddNew
)
}
)
Spacer(Modifier.height(8.dp))
@@ -72,14 +85,9 @@ private fun ReiterListContent(
items = uiState.searchResults,
columns = listOf(
MsColumnDefinition(
title = "Vorname",
weight = 1f,
cellRenderer = { Text(it.vorname, style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Nachname",
weight = 1f,
cellRenderer = { Text(it.nachname, style = MaterialTheme.typography.bodySmall) }
title = "Name",
weight = 1.5f,
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Lizenz",
@@ -103,6 +111,107 @@ private fun ReiterListContent(
}
}
@Composable
fun ReiterCard(
reiter: Reiter,
onEdit: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
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()) {
ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f))
ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
ReiterDetailItem(label = "E-Mail", value = reiter.email ?: "-", modifier = Modifier.weight(1f))
ReiterDetailItem(label = "Telefon", value = reiter.telefon ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
ReiterDetailItem(label = "Verein", value = reiter.verein ?: "-", modifier = Modifier.weight(1f))
ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f))
}
Spacer(Modifier.height(32.dp))
MsButton(
text = "Reiterdaten bearbeiten",
onClick = onEdit,
fullWidth = true
)
}
}
}
}
@Composable
private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface)
}
}
@Composable
private fun ReiterEditorContent(
uiState: ReiterUiState,
@@ -133,13 +242,15 @@ private fun ReiterEditorContent(
value = uiState.editVorname,
onValueChange = onVornameChange,
label = "Vorname",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editName,
onValueChange = onNachnameChange,
label = "Nachname",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -150,13 +261,15 @@ private fun ReiterEditorContent(
value = uiState.editFeiId,
onValueChange = onFeiIdChange,
label = "FEI ID",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editOepsNummer,
onValueChange = onOepsNummerChange,
label = "ÖPS Nummer",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -167,13 +280,15 @@ private fun ReiterEditorContent(
value = uiState.editGeburtsdatum,
onValueChange = onGeburtsdatumChange,
label = "Geburtsdatum",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editVerein,
onValueChange = onVereinChange,
label = "Verein",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -184,13 +299,15 @@ private fun ReiterEditorContent(
value = uiState.editEmail,
onValueChange = onEmailChange,
label = "E-Mail",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
MsTextField(
value = uiState.editTelefon,
onValueChange = onTelefonChange,
label = "Telefon",
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
compact = true
)
}
@@ -18,6 +18,7 @@ data class ReiterUiState(
val selectedReiter: Reiter? = null,
val isEditing: Boolean = false,
val isLoading: Boolean = false,
val editId: String = "",
val editName: String = "",
val editVorname: String = "",
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
@@ -65,6 +66,7 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
uiState = uiState.copy(
selectedReiter = reiter,
isEditing = true,
editId = reiter.id,
editVorname = reiter.vorname,
editName = reiter.nachname,
editLizenz = reiter.lizenz,
@@ -79,6 +81,25 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
)
}
fun addNewReiter() {
uiState = uiState.copy(
selectedReiter = null,
isEditing = true,
editId = "",
editVorname = "",
editName = "",
editLizenz = LizenzKlasse.KEINE,
editSparte = Sparte.KEINE,
editStatus = ReiterStatus.AKTIV,
editFeiId = "",
editOepsNummer = "",
editGeburtsdatum = "",
editEmail = "",
editTelefon = "",
editVerein = ""
)
}
fun onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }