From 345c329350cef25d70bdfcf7da9811cabee89ea8 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 20 Apr 2026 02:49:30 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20enhance=20Stammdaten-Verwaltung=20and?= =?UTF-8?q?=20refine=20desktop=20UX=20across=20multiple=20features,=20fix?= =?UTF-8?q?=20typo=20in=20`settings.json`,=20enable=20WASM=20builds,=20and?= =?UTF-8?q?=20add=20Master-Detail=20layout=20for=20Funktion=C3=A4re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...026-04-20_Stammdaten_Sidebar_Refinement.md | 36 +++ .../core/designsystem/components/MsButton.kt | 43 +++- .../designsystem/components/MsDataTable.kt | 8 +- .../funktionaer/di/FunktionaerModule.kt | 9 +- .../presentation/FunktionaerScreen.kt | 242 +++++++++++++++--- .../presentation/FunktionaerViewModel.kt | 76 ++++-- .../pferde/presentation/PferdeScreen.kt | 161 ++++++++++-- .../pferde/presentation/PferdeViewModel.kt | 19 ++ .../reiter/presentation/ReiterScreen.kt | 159 ++++++++++-- .../reiter/presentation/ReiterViewModel.kt | 21 ++ .../shells/meldestelle-desktop/settings.json | 2 +- .../at/mocode/frontend/shell/desktop/main.kt | 2 + .../screens/layout/DesktopMainLayout.kt | 91 +++++-- gradle.properties | 2 +- 14 files changed, 748 insertions(+), 123 deletions(-) create mode 100644 docs/99_Journal/2026-04-20_Stammdaten_Sidebar_Refinement.md diff --git a/docs/99_Journal/2026-04-20_Stammdaten_Sidebar_Refinement.md b/docs/99_Journal/2026-04-20_Stammdaten_Sidebar_Refinement.md new file mode 100644 index 00000000..0dd81866 --- /dev/null +++ b/docs/99_Journal/2026-04-20_Stammdaten_Sidebar_Refinement.md @@ -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). diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsButton.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsButton.kt index 4a8f718a..f176b036 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsButton.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsButton.kt @@ -1,12 +1,12 @@ package at.mocode.frontend.core.designsystem.components -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp enum class ButtonVariant { @@ -24,6 +24,7 @@ fun MsButton( modifier: Modifier = Modifier, variant: ButtonVariant = ButtonVariant.PRIMARY, size: ButtonSize = ButtonSize.MEDIUM, + icon: ImageVector? = null, enabled: Boolean = true, isLoading: Boolean = false, fullWidth: Boolean = false, @@ -44,34 +45,38 @@ fun MsButton( onClick = onClick, modifier = buttonModifier, enabled = enabled && !isLoading, + contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding, 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( onClick = onClick, modifier = buttonModifier, enabled = enabled && !isLoading, + contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding, 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( onClick = onClick, 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( onClick = onClick, 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 private fun ButtonContent( text: String, - isLoading: Boolean + isLoading: Boolean, + icon: ImageVector? = null ) { if (isLoading) { CircularProgressIndicator( - modifier = Modifier.padding(2.dp), - strokeWidth = 2.dp + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = LocalContentColor.current ) } 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) + } } } diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsDataTable.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsDataTable.kt index 7c41fd9c..fd6de87b 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsDataTable.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsDataTable.kt @@ -57,6 +57,7 @@ fun MsDataTable( items: List, columns: List>, onRowClick: ((T) -> Unit)? = null, + selectedItem: T? = null, modifier: Modifier = Modifier, headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant, rowBackgroundColor: Color = MaterialTheme.colorScheme.surface, @@ -100,7 +101,12 @@ fun MsDataTable( val state = androidx.compose.foundation.lazy.rememberLazyListState() LazyColumn(state = state, modifier = Modifier.fillMaxSize()) { 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( color = bgColor, diff --git a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/di/FunktionaerModule.kt b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/di/FunktionaerModule.kt index 23ce78fd..e4375595 100644 --- a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/di/FunktionaerModule.kt +++ b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/di/FunktionaerModule.kt @@ -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 = 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 = 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) ) } diff --git a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt index 0b96681e..4630cf97 100644 --- a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt +++ b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt @@ -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) } } diff --git a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt index 153d3e06..84dfb565 100644 --- a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt +++ b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt @@ -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 = emptyList(), - val filtered: List = emptyList(), - val selectedId: Long? = null, + val list: List = emptyList(), + val filtered: List = 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 + suspend fun list(): List } class FunktionaerViewModel( private val repo: FunktionaerRepository, -) { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - +) : ViewModel() { private val _state = MutableStateFlow(FunktionaerState(isLoading = true)) val state: StateFlow = _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, query: String): List { + private fun filterList(list: List, query: String): List { 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) } } diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt index 690d6476..ad042273 100644 --- a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt @@ -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 ) } diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt index e4411ca2..fa8703a1 100644 --- a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt @@ -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) } diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt index e9087071..18c0d7c7 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt @@ -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 ) } diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt index c4e57659..11003d86 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt @@ -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) } diff --git a/frontend/shells/meldestelle-desktop/settings.json b/frontend/shells/meldestelle-desktop/settings.json index 91f2e44b..2fd53b82 100644 --- a/frontend/shells/meldestelle-desktop/settings.json +++ b/frontend/shells/meldestelle-desktop/settings.json @@ -1,6 +1,6 @@ { "deviceName": "Meldestelle", - "sharedKey": "Paassword", + "sharedKey": "Password", "backupPath": "/mocode/meldestelle/docs/temp", "networkRole": "MASTER", "expectedClients": [ diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt index 54ee9e2e..01a8e6d1 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt @@ -12,6 +12,7 @@ import at.mocode.frontend.core.network.networkModule import at.mocode.frontend.core.sync.di.syncModule import at.mocode.frontend.features.billing.di.billingModule 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.pferde.di.pferdeModule import at.mocode.frontend.features.profile.di.profileModule @@ -42,6 +43,7 @@ fun main() = application { billingModule, pferdeModule, reiterModule, + funktionaerModule, vereinFeatureModule, turnierFeatureModule, deviceInitializationModule, diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt index 97c8d302..1b324766 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.presentation.DeviceInitializationScreen 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.NennungViewModel import at.mocode.frontend.features.pferde.presentation.PferdeScreen @@ -176,12 +180,64 @@ private fun DesktopNavRail( ) NavRailItem( - icon = Icons.Default.People, - label = "Vereine", - selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung, - onClick = { onNavigate(AppScreen.Vereine) } + icon = Icons.Default.CloudDownload, + label = "ZNS-Import", + selected = currentScreen is AppScreen.StammdatenImport, + 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( icon = Icons.Default.Email, label = "Mails", @@ -573,7 +629,7 @@ private fun DesktopContentArea( } // --- Pferde-Verwaltung & Profil --- - is AppScreen.PferdVerwaltung -> { + is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> { val viewModel = koinViewModel() PferdeScreen(viewModel = viewModel) } @@ -591,7 +647,7 @@ private fun DesktopContentArea( } // --- Reiter-Verwaltung & Profil --- - is AppScreen.ReiterVerwaltung -> { + is AppScreen.Reiter, is AppScreen.ReiterVerwaltung -> { val viewModel = koinViewModel() ReiterScreen(viewModel = viewModel) } @@ -607,7 +663,7 @@ private fun DesktopContentArea( } // --- Verein-Verwaltung & Profil --- - is AppScreen.VereinVerwaltung -> { + is AppScreen.Vereine, is AppScreen.VereinVerwaltung -> { println("[Screen] Rendering VereinVerwaltung (VereinScreen)") val vereinViewModel: VereinViewModel = koinViewModel() VereinScreen(viewModel = vereinViewModel) @@ -621,15 +677,20 @@ private fun DesktopContentArea( } // --- Funktionaer-Verwaltung & Profil --- - is AppScreen.FunktionaerVerwaltung -> FunktionaerVerwaltungScreen( - onBack = onBack, - onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) } - ) + is AppScreen.FunktionaerVerwaltung -> { + val viewModel = koinViewModel() + FunktionaerScreen(viewModel = viewModel) + } - is AppScreen.FunktionaerProfil -> FunktionaerProfil( - id = currentScreen.id, - onBack = onBack, - ) + is AppScreen.FunktionaerProfil -> { + val viewModel = koinViewModel() + LaunchedEffect(currentScreen.id) { + viewModel.state.value.list.find { it.id == currentScreen.id }?.let { + viewModel.send(FunktionaerIntent.Select(it)) + } + } + FunktionaerScreen(viewModel = viewModel) + } // --- Veranstalter-Verwaltung & Profil --- is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen( diff --git a/gradle.properties b/gradle.properties index 3b180d2e..e9d42f39 100644 --- a/gradle.properties +++ b/gradle.properties @@ -73,7 +73,7 @@ dev.port.offset=0 # ------------------------------------------------------------------ # Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische # 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) # See https://kotl.in/dokka-gradle-migration