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