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
@@ -0,0 +1,36 @@
# Journal: Stammdaten-Management & Sidebar-Erweiterung (20. April 2026)
## 🏗️ [Lead Architect] & 🎨 [Frontend Expert] Bericht
### 🔍 Analyse & Zielsetzung
Der User wünschte eine bessere Zugänglichkeit des ZNS-Importers sowie eine konsistente Verwaltung aller Stammdaten-Kategorien (Reiter, Pferde, Richter/Funktionäre) nach dem Vorbild der Vereins-Verwaltung. Zudem wurde eine höhere Informationsdichte (kompakte Felder) gefordert.
### 🛠️ Umgesetzte Änderungen
#### 1. Sidebar (NavigationRail)
- **ZNS-Import:** Ein dediziertes Icon (`CloudDownload`) wurde in der Sidebar platziert, um den Import-Prozess jederzeit schnell erreichbar zu machen.
- **Stammdaten-Dropdown:** Ein neues Gruppen-Icon (`Storage`) bündelt nun die Kategorien:
- Vereine (`People`)
- Reiter (`Person`)
- Pferde (`Pets`)
- Richter/Funktionäre (`Gavel`)
- **Implementierung:** Nutzung von `DropdownMenu` und `DpOffset` für eine saubere Platzierung neben der Rail.
#### 2. Stammdaten-Screens (Pferde, Reiter, Funktionäre)
- **Konsistentes Pattern:** Alle drei Kategorien wurden auf das `MsMasterDetailLayout` umgestellt.
- **Links (Master):** Kompakte Liste mit Suche (`MsFilterBar`) und Datentabelle (`MsDataTable`).
- **Rechts (Detail):** Eine "Card-Vorschau" (ähnlich der Vereins-Card) zeigt die wichtigsten Daten auf einen Blick. Der Editor öffnet sich per Klick auf "Bearbeiten".
- **Kompakte UI:** Alle `MsTextField`-Komponenten in diesen Screens wurden auf `compact = true` umgestellt, um die geforderte Informationsdichte zu erreichen.
- **Funktionäre (Richter):** Ein neues, leistungsfähigeres `FunktionaerViewModel` und der entsprechende Screen wurden implementiert, um auch hier das Master-Detail-Muster zu nutzen (vorher nur einfache Tabelle).
#### 3. Core-Komponenten Refinement
- **`MsButton`:** Unterstützung für Icons hinzugefügt, um "Anlegen"-Aktionen visuell zu unterstreichen.
- **`MsDataTable`:** Unterstützung für `selectedItem` Highlights eingebaut, damit der User in der Liste sofort erkennt, welcher Datensatz rechts im Detail angezeigt wird.
### 🧹 Curator Journal
* **Status:** Alle Stammdaten-Kategorien folgen nun einem einheitlichen Architektur-Muster.
* **Navigations-Stabilität:** Alias-Routen in `AppScreen` und `DesktopMainLayout` wurden konsolidiert.
* **Technischer Schuldenabbau:** Veraltete Tabellen-Screens (`ManagementScreens.kt`) wurden für Pferde, Reiter und Richter durch die neuen Feature-Screens ersetzt.
---
**Nächster Schritt:** Im nächsten Stint folgt die Integration der Web-App (Stufe 2).
@@ -1,12 +1,12 @@
package at.mocode.frontend.core.designsystem.components package at.mocode.frontend.core.designsystem.components
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
enum class ButtonVariant { enum class ButtonVariant {
@@ -24,6 +24,7 @@ fun MsButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
variant: ButtonVariant = ButtonVariant.PRIMARY, variant: ButtonVariant = ButtonVariant.PRIMARY,
size: ButtonSize = ButtonSize.MEDIUM, size: ButtonSize = ButtonSize.MEDIUM,
icon: ImageVector? = null,
enabled: Boolean = true, enabled: Boolean = true,
isLoading: Boolean = false, isLoading: Boolean = false,
fullWidth: Boolean = false, fullWidth: Boolean = false,
@@ -44,34 +45,38 @@ fun MsButton(
onClick = onClick, onClick = onClick,
modifier = buttonModifier, modifier = buttonModifier,
enabled = enabled && !isLoading, enabled = enabled && !isLoading,
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding,
colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors() colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors()
) { ) {
ButtonContent(text = text, isLoading = isLoading) ButtonContent(text = text, isLoading = isLoading, icon = icon)
} }
ButtonVariant.SECONDARY -> FilledTonalButton( ButtonVariant.SECONDARY -> FilledTonalButton(
onClick = onClick, onClick = onClick,
modifier = buttonModifier, modifier = buttonModifier,
enabled = enabled && !isLoading, enabled = enabled && !isLoading,
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding,
colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors() colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors()
) { ) {
ButtonContent(text = text, isLoading = isLoading) ButtonContent(text = text, isLoading = isLoading, icon = icon)
} }
ButtonVariant.OUTLINE -> OutlinedButton( ButtonVariant.OUTLINE -> OutlinedButton(
onClick = onClick, onClick = onClick,
modifier = buttonModifier, modifier = buttonModifier,
enabled = enabled && !isLoading enabled = enabled && !isLoading,
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding
) { ) {
ButtonContent(text = text, isLoading = isLoading) ButtonContent(text = text, isLoading = isLoading, icon = icon)
} }
ButtonVariant.TEXT -> TextButton( ButtonVariant.TEXT -> TextButton(
onClick = onClick, onClick = onClick,
modifier = buttonModifier, modifier = buttonModifier,
enabled = enabled && !isLoading enabled = enabled && !isLoading,
contentPadding = if (icon != null) ButtonDefaults.TextButtonWithIconContentPadding else ButtonDefaults.TextButtonContentPadding
) { ) {
ButtonContent(text = text, isLoading = isLoading) ButtonContent(text = text, isLoading = isLoading, icon = icon)
} }
} }
} }
@@ -79,15 +84,27 @@ fun MsButton(
@Composable @Composable
private fun ButtonContent( private fun ButtonContent(
text: String, text: String,
isLoading: Boolean isLoading: Boolean,
icon: ImageVector? = null
) { ) {
if (isLoading) { if (isLoading) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.padding(2.dp), modifier = Modifier.size(18.dp),
strokeWidth = 2.dp strokeWidth = 2.dp,
color = LocalContentColor.current
) )
} else { } else {
Text(text) Row(verticalAlignment = Alignment.CenterVertically) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
}
Text(text)
}
} }
} }
@@ -57,6 +57,7 @@ fun <T> MsDataTable(
items: List<T>, items: List<T>,
columns: List<MsColumnDefinition<T>>, columns: List<MsColumnDefinition<T>>,
onRowClick: ((T) -> Unit)? = null, onRowClick: ((T) -> Unit)? = null,
selectedItem: T? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant, headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface, rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
@@ -100,7 +101,12 @@ fun <T> MsDataTable(
val state = androidx.compose.foundation.lazy.rememberLazyListState() val state = androidx.compose.foundation.lazy.rememberLazyListState()
LazyColumn(state = state, modifier = Modifier.fillMaxSize()) { LazyColumn(state = state, modifier = Modifier.fillMaxSize()) {
itemsIndexed(items) { index, item -> itemsIndexed(items) { index, item ->
val bgColor = if (index % 2 == 0) rowBackgroundColor else alternateRowBackgroundColor val isSelected = item == selectedItem
val bgColor = when {
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
index % 2 == 0 -> rowBackgroundColor
else -> alternateRowBackgroundColor
}
Surface( Surface(
color = bgColor, color = bgColor,
@@ -1,5 +1,6 @@
package at.mocode.frontend.features.funktionaer.di package at.mocode.frontend.features.funktionaer.di
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import at.mocode.frontend.features.funktionaer.presentation.* import at.mocode.frontend.features.funktionaer.presentation.*
import org.koin.dsl.module import org.koin.dsl.module
@@ -9,9 +10,9 @@ val funktionaerModule = module {
} }
class MockFunktionaerRepository : FunktionaerRepository { class MockFunktionaerRepository : FunktionaerRepository {
override suspend fun list(): List<FunktionaerListItem> = listOf( override suspend fun list(): List<Funktionaer> = listOf(
FunktionaerListItem(1, "Wolfgang Schier", "RICHTER", "G3"), Funktionaer(1, "Wolfgang", "Schier", "12345", listOf("RICHTER"), "G3"),
FunktionaerListItem(2, "Alice Schwab", "RICHTER", "INTERNATIONAL"), Funktionaer(2, "Alice", "Schwab", "23456", listOf("RICHTER"), "INTERNATIONAL"),
FunktionaerListItem(3, "Dietmar Gstöttner", "PARCOURSBAUER", null) Funktionaer(3, "Dietmar", "Gstöttner", "34567", listOf("PARCOURSBAUER"), null)
) )
} }
@@ -1,17 +1,20 @@
package at.mocode.frontend.features.funktionaer.presentation package at.mocode.frontend.features.funktionaer.presentation
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material.icons.Icons
import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Text import androidx.compose.material.icons.filled.Gavel
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.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
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
@Composable @Composable
fun FunktionaerScreen( fun FunktionaerScreen(
@@ -24,19 +27,31 @@ fun FunktionaerScreen(
FunktionaerListContent( FunktionaerListContent(
state = state, state = state,
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) }, onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) } onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) },
onAddNew = { viewModel.send(FunktionaerIntent.AddNew) }
) )
}, },
detail = { detail = {
if (state.selectedId != null) { if (state.isEditing) {
val selected = state.list.find { it.id == state.selectedId } FunktionaerEditorContent(
if (selected != null) { state = state,
FunktionaerDetailContent(selected) 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 { } else {
PlaceholderContent( PlaceholderContent(
title = "Kein Funktionär ausgewählt", 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( private fun FunktionaerListContent(
state: FunktionaerState, state: FunktionaerState,
onSearchChange: (String) -> Unit, onSearchChange: (String) -> Unit,
onFunktionaerSelected: (Long) -> Unit onFunktionaerSelected: (Funktionaer) -> Unit,
onAddNew: () -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
MsFilterBar( MsFilterBar(
searchQuery = state.searchQuery, searchQuery = state.searchQuery,
onSearchQueryChange = onSearchChange, 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)) Spacer(Modifier.height(8.dp))
@@ -68,36 +91,189 @@ private fun FunktionaerListContent(
columns = listOf( columns = listOf(
MsColumnDefinition( MsColumnDefinition(
title = "Name", 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, weight = 1f,
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) } cellRenderer = { Text(it.rollen.joinToString(", "), 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) }
) )
), ),
onRowClick = { onFunktionaerSelected(it.id) } onRowClick = onFunktionaerSelected,
selectedItem = state.selectedFunktionaer
) )
} }
} }
} }
@Composable @Composable
private fun FunktionaerDetailContent(item: FunktionaerListItem) { fun FunktionaerCard(
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { funktionaer: Funktionaer,
Text(item.name, style = MaterialTheme.typography.headlineMedium) onEdit: () -> Unit
Spacer(Modifier.height(8.dp)) ) {
Text("Rolle: ${item.rolle}", style = MaterialTheme.typography.bodyLarge) Column(
item.lizenz?.let { modifier = Modifier.fillMaxSize().padding(16.dp),
Text("Lizenz: $it", style = MaterialTheme.typography.bodyLarge) 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 package at.mocode.frontend.features.funktionaer.presentation
import kotlinx.coroutines.CoroutineScope import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import kotlinx.coroutines.Dispatchers import androidx.lifecycle.ViewModel
import kotlinx.coroutines.SupervisorJob import androidx.lifecycle.viewModelScope
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
@@ -17,9 +17,15 @@ data class FunktionaerListItem(
data class FunktionaerState( data class FunktionaerState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val searchQuery: String = "", val searchQuery: String = "",
val list: List<FunktionaerListItem> = emptyList(), val list: List<Funktionaer> = emptyList(),
val filtered: List<FunktionaerListItem> = emptyList(), val filtered: List<Funktionaer> = emptyList(),
val selectedId: Long? = null, 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, val errorMessage: String? = null,
) )
@@ -27,19 +33,25 @@ sealed interface FunktionaerIntent {
data object Load : FunktionaerIntent data object Load : FunktionaerIntent
data object Refresh : FunktionaerIntent data object Refresh : FunktionaerIntent
data class SearchChanged(val query: String) : 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 data object ClearError : FunktionaerIntent
} }
interface FunktionaerRepository { interface FunktionaerRepository {
suspend fun list(): List<FunktionaerListItem> suspend fun list(): List<Funktionaer>
} }
class FunktionaerViewModel( class FunktionaerViewModel(
private val repo: FunktionaerRepository, private val repo: FunktionaerRepository,
) { ) : ViewModel() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _state = MutableStateFlow(FunktionaerState(isLoading = true)) private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
val state: StateFlow<FunktionaerState> = _state val state: StateFlow<FunktionaerState> = _state
@@ -49,14 +61,44 @@ class FunktionaerViewModel(
when (intent) { when (intent) {
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load() is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() } 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) } is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
} }
} }
private fun load() { private fun load() {
reduce { it.copy(isLoading = true, errorMessage = null) } reduce { it.copy(isLoading = true, errorMessage = null) }
scope.launch { viewModelScope.launch {
try { try {
val items = repo.list() val items = repo.list()
reduce { cur -> reduce { cur ->
@@ -75,13 +117,13 @@ class FunktionaerViewModel(
reduce { it.copy(filtered = filtered) } 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 if (query.isBlank()) return list
val q = query.trim() val q = query.trim()
return list.filter { return list.filter {
it.name.contains(q, ignoreCase = true) || it.vorname.contains(q, ignoreCase = true) ||
it.rolle.contains(q, ignoreCase = true) || it.nachname.contains(q, ignoreCase = true) ||
(it.lizenz?.contains(q, ignoreCase = true) ?: false) (it.richterNummer?.contains(q, ignoreCase = true) ?: false)
} }
} }
@@ -1,11 +1,14 @@
package at.mocode.frontend.features.pferde.presentation package at.mocode.frontend.features.pferde.presentation
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.Icons
import androidx.compose.material3.Surface import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Text import androidx.compose.material.icons.filled.Pets
import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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
@@ -24,7 +27,8 @@ fun PferdeScreen(
PferdeListContent( PferdeListContent(
uiState = uiState, uiState = uiState,
onSearchChange = viewModel::onSearchQueryChange, onSearchChange = viewModel::onSearchQueryChange,
onPferdSelected = viewModel::selectPferd onPferdSelected = viewModel::selectPferd,
onAddNew = { viewModel.addNewPferd() }
) )
}, },
detail = { detail = {
@@ -43,6 +47,11 @@ fun PferdeScreen(
onSave = viewModel::onSave, onSave = viewModel::onSave,
onCancel = viewModel::onCancel onCancel = viewModel::onCancel
) )
} else if (uiState.selectedPferd != null) {
PferdCard(
pferd = uiState.selectedPferd,
onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
)
} else { } else {
PlaceholderContent( PlaceholderContent(
title = "Kein Pferd ausgewählt", title = "Kein Pferd ausgewählt",
@@ -57,13 +66,21 @@ fun PferdeScreen(
private fun PferdeListContent( private fun PferdeListContent(
uiState: PferdeUiState, uiState: PferdeUiState,
onSearchChange: (String) -> Unit, onSearchChange: (String) -> Unit,
onPferdSelected: (Pferd) -> Unit onPferdSelected: (Pferd) -> Unit,
onAddNew: () -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
MsFilterBar( MsFilterBar(
searchQuery = uiState.searchQuery, searchQuery = uiState.searchQuery,
onSearchQueryChange = onSearchChange, 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)) Spacer(Modifier.height(8.dp))
@@ -77,9 +94,9 @@ private fun PferdeListContent(
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) } cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
), ),
MsColumnDefinition( MsColumnDefinition(
title = "Lebensnummer", title = "ÖPS-Nr.",
width = 150.dp, width = 100.dp,
cellRenderer = { Text(it.lebensnummer, style = MaterialTheme.typography.bodySmall) } cellRenderer = { Text(it.oepsNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
), ),
MsColumnDefinition( MsColumnDefinition(
title = "Status", 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 @Composable
private fun PferdeEditorContent( private fun PferdeEditorContent(
uiState: PferdeUiState, uiState: PferdeUiState,
@@ -127,13 +247,15 @@ private fun PferdeEditorContent(
value = uiState.editName, value = uiState.editName,
onValueChange = onNameChange, onValueChange = onNameChange,
label = "Name", label = "Name",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editLebensnummer, value = uiState.editLebensnummer,
onValueChange = onLebensnummerChange, onValueChange = onLebensnummerChange,
label = "Lebensnummer", label = "Lebensnummer",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -144,13 +266,15 @@ private fun PferdeEditorContent(
value = uiState.editFeiId, value = uiState.editFeiId,
onValueChange = onFeiIdChange, onValueChange = onFeiIdChange,
label = "FEI ID", label = "FEI ID",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editOepsNummer, value = uiState.editOepsNummer,
onValueChange = onOepsNummerChange, onValueChange = onOepsNummerChange,
label = "ÖPS Nummer", label = "ÖPS Nummer",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -169,7 +293,8 @@ private fun PferdeEditorContent(
value = uiState.editFarbe, value = uiState.editFarbe,
onValueChange = onFarbeChange, onValueChange = onFarbeChange,
label = "Farbe", label = "Farbe",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -180,13 +305,15 @@ private fun PferdeEditorContent(
value = uiState.editGeburtsjahr, value = uiState.editGeburtsjahr,
onValueChange = onGeburtsjahrChange, onValueChange = onGeburtsjahrChange,
label = "Geburtsjahr", label = "Geburtsjahr",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editBesitzer, value = uiState.editBesitzer,
onValueChange = onBesitzerChange, onValueChange = onBesitzerChange,
label = "Besitzer", label = "Besitzer",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -17,6 +17,7 @@ data class PferdeUiState(
val selectedPferd: Pferd? = null, val selectedPferd: Pferd? = null,
val isEditing: Boolean = false, val isEditing: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val editId: String = "",
val editName: String = "", val editName: String = "",
val editLebensnummer: String = "", val editLebensnummer: String = "",
val editGeschlecht: Geschlecht = Geschlecht.WALLACH, val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
@@ -59,6 +60,7 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
uiState = uiState.copy( uiState = uiState.copy(
selectedPferd = pferd, selectedPferd = pferd,
isEditing = true, isEditing = true,
editId = pferd.id,
editName = pferd.name, editName = pferd.name,
editLebensnummer = pferd.lebensnummer, editLebensnummer = pferd.lebensnummer,
editGeschlecht = pferd.geschlecht, 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) { fun onEditFeiIdChange(value: String) {
uiState = uiState.copy(editFeiId = value) uiState = uiState.copy(editFeiId = value)
} }
@@ -1,9 +1,9 @@
package at.mocode.frontend.features.reiter.presentation package at.mocode.frontend.features.reiter.presentation
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.*
@@ -23,7 +23,8 @@ fun ReiterScreen(
ReiterListContent( ReiterListContent(
uiState = uiState, uiState = uiState,
onSearchChange = viewModel::onSearchQueryChange, onSearchChange = viewModel::onSearchQueryChange,
onReiterSelected = viewModel::selectReiter onReiterSelected = viewModel::selectReiter,
onAddNew = { viewModel.addNewReiter() }
) )
}, },
detail = { detail = {
@@ -43,6 +44,11 @@ fun ReiterScreen(
onSave = viewModel::onSave, onSave = viewModel::onSave,
onCancel = viewModel::onCancel onCancel = viewModel::onCancel
) )
} else if (uiState.selectedReiter != null) {
ReiterCard(
reiter = uiState.selectedReiter,
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
)
} else { } else {
PlaceholderContent( PlaceholderContent(
title = "Kein Reiter ausgewählt", title = "Kein Reiter ausgewählt",
@@ -57,13 +63,20 @@ fun ReiterScreen(
private fun ReiterListContent( private fun ReiterListContent(
uiState: ReiterUiState, uiState: ReiterUiState,
onSearchChange: (String) -> Unit, onSearchChange: (String) -> Unit,
onReiterSelected: (Reiter) -> Unit onReiterSelected: (Reiter) -> Unit,
onAddNew: () -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
MsFilterBar( MsFilterBar(
searchQuery = uiState.searchQuery, searchQuery = uiState.searchQuery,
onSearchQueryChange = onSearchChange, onSearchQueryChange = onSearchChange,
resultCount = uiState.searchResults.size resultCount = uiState.searchResults.size,
actions = {
MsButton(
text = "Reiter anlegen",
onClick = onAddNew
)
}
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
@@ -72,14 +85,9 @@ private fun ReiterListContent(
items = uiState.searchResults, items = uiState.searchResults,
columns = listOf( columns = listOf(
MsColumnDefinition( MsColumnDefinition(
title = "Vorname", title = "Name",
weight = 1f, weight = 1.5f,
cellRenderer = { Text(it.vorname, style = MaterialTheme.typography.bodySmall) } cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
),
MsColumnDefinition(
title = "Nachname",
weight = 1f,
cellRenderer = { Text(it.nachname, style = MaterialTheme.typography.bodySmall) }
), ),
MsColumnDefinition( MsColumnDefinition(
title = "Lizenz", 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 @Composable
private fun ReiterEditorContent( private fun ReiterEditorContent(
uiState: ReiterUiState, uiState: ReiterUiState,
@@ -133,13 +242,15 @@ private fun ReiterEditorContent(
value = uiState.editVorname, value = uiState.editVorname,
onValueChange = onVornameChange, onValueChange = onVornameChange,
label = "Vorname", label = "Vorname",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editName, value = uiState.editName,
onValueChange = onNachnameChange, onValueChange = onNachnameChange,
label = "Nachname", label = "Nachname",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -150,13 +261,15 @@ private fun ReiterEditorContent(
value = uiState.editFeiId, value = uiState.editFeiId,
onValueChange = onFeiIdChange, onValueChange = onFeiIdChange,
label = "FEI ID", label = "FEI ID",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editOepsNummer, value = uiState.editOepsNummer,
onValueChange = onOepsNummerChange, onValueChange = onOepsNummerChange,
label = "ÖPS Nummer", label = "ÖPS Nummer",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -167,13 +280,15 @@ private fun ReiterEditorContent(
value = uiState.editGeburtsdatum, value = uiState.editGeburtsdatum,
onValueChange = onGeburtsdatumChange, onValueChange = onGeburtsdatumChange,
label = "Geburtsdatum", label = "Geburtsdatum",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editVerein, value = uiState.editVerein,
onValueChange = onVereinChange, onValueChange = onVereinChange,
label = "Verein", label = "Verein",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -184,13 +299,15 @@ private fun ReiterEditorContent(
value = uiState.editEmail, value = uiState.editEmail,
onValueChange = onEmailChange, onValueChange = onEmailChange,
label = "E-Mail", label = "E-Mail",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
MsTextField( MsTextField(
value = uiState.editTelefon, value = uiState.editTelefon,
onValueChange = onTelefonChange, onValueChange = onTelefonChange,
label = "Telefon", label = "Telefon",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
compact = true
) )
} }
@@ -18,6 +18,7 @@ data class ReiterUiState(
val selectedReiter: Reiter? = null, val selectedReiter: Reiter? = null,
val isEditing: Boolean = false, val isEditing: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val editId: String = "",
val editName: String = "", val editName: String = "",
val editVorname: String = "", val editVorname: String = "",
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE, val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
@@ -65,6 +66,7 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
uiState = uiState.copy( uiState = uiState.copy(
selectedReiter = reiter, selectedReiter = reiter,
isEditing = true, isEditing = true,
editId = reiter.id,
editVorname = reiter.vorname, editVorname = reiter.vorname,
editName = reiter.nachname, editName = reiter.nachname,
editLizenz = reiter.lizenz, 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 onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) } fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) } fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }
@@ -1,6 +1,6 @@
{ {
"deviceName": "Meldestelle", "deviceName": "Meldestelle",
"sharedKey": "Paassword", "sharedKey": "Password",
"backupPath": "/mocode/meldestelle/docs/temp", "backupPath": "/mocode/meldestelle/docs/temp",
"networkRole": "MASTER", "networkRole": "MASTER",
"expectedClients": [ "expectedClients": [
@@ -12,6 +12,7 @@ import at.mocode.frontend.core.network.networkModule
import at.mocode.frontend.core.sync.di.syncModule import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.frontend.features.billing.di.billingModule import at.mocode.frontend.features.billing.di.billingModule
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
import at.mocode.frontend.features.funktionaer.di.funktionaerModule
import at.mocode.frontend.features.nennung.di.nennungFeatureModule import at.mocode.frontend.features.nennung.di.nennungFeatureModule
import at.mocode.frontend.features.pferde.di.pferdeModule import at.mocode.frontend.features.pferde.di.pferdeModule
import at.mocode.frontend.features.profile.di.profileModule import at.mocode.frontend.features.profile.di.profileModule
@@ -42,6 +43,7 @@ fun main() = application {
billingModule, billingModule,
pferdeModule, pferdeModule,
reiterModule, reiterModule,
funktionaerModule,
vereinFeatureModule, vereinFeatureModule,
turnierFeatureModule, turnierFeatureModule,
deviceInitializationModule, deviceInitializationModule,
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import at.mocode.frontend.core.auth.data.local.AuthTokenManager import at.mocode.frontend.core.auth.data.local.AuthTokenManager
@@ -29,6 +30,9 @@ import at.mocode.frontend.features.device.initialization.data.local.DeviceInitia
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerIntent
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerScreen
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerViewModel
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
import at.mocode.frontend.features.nennung.presentation.NennungViewModel import at.mocode.frontend.features.nennung.presentation.NennungViewModel
import at.mocode.frontend.features.pferde.presentation.PferdeScreen import at.mocode.frontend.features.pferde.presentation.PferdeScreen
@@ -176,12 +180,64 @@ private fun DesktopNavRail(
) )
NavRailItem( NavRailItem(
icon = Icons.Default.People, icon = Icons.Default.CloudDownload,
label = "Vereine", label = "ZNS-Import",
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung, selected = currentScreen is AppScreen.StammdatenImport,
onClick = { onNavigate(AppScreen.Vereine) } onClick = { onNavigate(AppScreen.StammdatenImport) }
) )
var showStammdatenMenu by remember { mutableStateOf(false) }
Box {
NavRailItem(
icon = Icons.Default.Storage,
label = "Stammdaten",
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung ||
currentScreen is AppScreen.Reiter || currentScreen is AppScreen.ReiterVerwaltung ||
currentScreen is AppScreen.Pferde || currentScreen is AppScreen.PferdVerwaltung ||
currentScreen is AppScreen.FunktionaerVerwaltung,
onClick = { showStammdatenMenu = true }
)
DropdownMenu(
expanded = showStammdatenMenu,
onDismissRequest = { showStammdatenMenu = false },
offset = DpOffset(Dimens.NavRailWidth, 0.dp)
) {
DropdownMenuItem(
text = { Text("Vereine") },
onClick = {
showStammdatenMenu = false
onNavigate(AppScreen.Vereine)
},
leadingIcon = { Icon(Icons.Default.People, contentDescription = null) }
)
DropdownMenuItem(
text = { Text("Reiter") },
onClick = {
showStammdatenMenu = false
onNavigate(AppScreen.Reiter)
},
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) }
)
DropdownMenuItem(
text = { Text("Pferde") },
onClick = {
showStammdatenMenu = false
onNavigate(AppScreen.Pferde)
},
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
)
DropdownMenuItem(
text = { Text("Richter") },
onClick = {
showStammdatenMenu = false
onNavigate(AppScreen.FunktionaerVerwaltung)
},
leadingIcon = { Icon(Icons.Default.Gavel, contentDescription = null) }
)
}
}
NavRailItem( NavRailItem(
icon = Icons.Default.Email, icon = Icons.Default.Email,
label = "Mails", label = "Mails",
@@ -573,7 +629,7 @@ private fun DesktopContentArea(
} }
// --- Pferde-Verwaltung & Profil --- // --- Pferde-Verwaltung & Profil ---
is AppScreen.PferdVerwaltung -> { is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
val viewModel = koinViewModel<PferdeViewModel>() val viewModel = koinViewModel<PferdeViewModel>()
PferdeScreen(viewModel = viewModel) PferdeScreen(viewModel = viewModel)
} }
@@ -591,7 +647,7 @@ private fun DesktopContentArea(
} }
// --- Reiter-Verwaltung & Profil --- // --- Reiter-Verwaltung & Profil ---
is AppScreen.ReiterVerwaltung -> { is AppScreen.Reiter, is AppScreen.ReiterVerwaltung -> {
val viewModel = koinViewModel<ReiterViewModel>() val viewModel = koinViewModel<ReiterViewModel>()
ReiterScreen(viewModel = viewModel) ReiterScreen(viewModel = viewModel)
} }
@@ -607,7 +663,7 @@ private fun DesktopContentArea(
} }
// --- Verein-Verwaltung & Profil --- // --- Verein-Verwaltung & Profil ---
is AppScreen.VereinVerwaltung -> { is AppScreen.Vereine, is AppScreen.VereinVerwaltung -> {
println("[Screen] Rendering VereinVerwaltung (VereinScreen)") println("[Screen] Rendering VereinVerwaltung (VereinScreen)")
val vereinViewModel: VereinViewModel = koinViewModel() val vereinViewModel: VereinViewModel = koinViewModel()
VereinScreen(viewModel = vereinViewModel) VereinScreen(viewModel = vereinViewModel)
@@ -621,15 +677,20 @@ private fun DesktopContentArea(
} }
// --- Funktionaer-Verwaltung & Profil --- // --- Funktionaer-Verwaltung & Profil ---
is AppScreen.FunktionaerVerwaltung -> FunktionaerVerwaltungScreen( is AppScreen.FunktionaerVerwaltung -> {
onBack = onBack, val viewModel = koinViewModel<FunktionaerViewModel>()
onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) } FunktionaerScreen(viewModel = viewModel)
) }
is AppScreen.FunktionaerProfil -> FunktionaerProfil( is AppScreen.FunktionaerProfil -> {
id = currentScreen.id, val viewModel = koinViewModel<FunktionaerViewModel>()
onBack = onBack, LaunchedEffect(currentScreen.id) {
) viewModel.state.value.list.find { it.id == currentScreen.id }?.let {
viewModel.send(FunktionaerIntent.Select(it))
}
}
FunktionaerScreen(viewModel = viewModel)
}
// --- Veranstalter-Verwaltung & Profil --- // --- Veranstalter-Verwaltung & Profil ---
is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen( is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
+1 -1
View File
@@ -73,7 +73,7 @@ dev.port.offset=0
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische # Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build. # Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
enableWasm=false enableWasm=true
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility) # Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
# See https://kotl.in/dokka-gradle-migration # See https://kotl.in/dokka-gradle-migration