chore: enhance Stammdaten-Verwaltung and refine desktop UX across multiple features, fix typo in settings.json, enable WASM builds, and add Master-Detail layout for Funktionäre
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
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:
@@ -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).
|
||||||
+30
-13
@@ -1,12 +1,12 @@
|
|||||||
package at.mocode.frontend.core.designsystem.components
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
enum class ButtonVariant {
|
enum class ButtonVariant {
|
||||||
@@ -24,6 +24,7 @@ fun MsButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
variant: ButtonVariant = ButtonVariant.PRIMARY,
|
variant: ButtonVariant = ButtonVariant.PRIMARY,
|
||||||
size: ButtonSize = ButtonSize.MEDIUM,
|
size: ButtonSize = ButtonSize.MEDIUM,
|
||||||
|
icon: ImageVector? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
fullWidth: Boolean = false,
|
fullWidth: Boolean = false,
|
||||||
@@ -44,34 +45,38 @@ fun MsButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading,
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding,
|
||||||
colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors()
|
colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors()
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonVariant.SECONDARY -> FilledTonalButton(
|
ButtonVariant.SECONDARY -> FilledTonalButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading,
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding,
|
||||||
colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors()
|
colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors()
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonVariant.OUTLINE -> OutlinedButton(
|
ButtonVariant.OUTLINE -> OutlinedButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonVariant.TEXT -> TextButton(
|
ButtonVariant.TEXT -> TextButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.TextButtonWithIconContentPadding else ButtonDefaults.TextButtonContentPadding
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,15 +84,27 @@ fun MsButton(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ButtonContent(
|
private fun ButtonContent(
|
||||||
text: String,
|
text: String,
|
||||||
isLoading: Boolean
|
isLoading: Boolean,
|
||||||
|
icon: ImageVector? = null
|
||||||
) {
|
) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.padding(2.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
strokeWidth = 2.dp
|
strokeWidth = 2.dp,
|
||||||
|
color = LocalContentColor.current
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(text)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(ButtonDefaults.IconSize)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -57,6 +57,7 @@ fun <T> MsDataTable(
|
|||||||
items: List<T>,
|
items: List<T>,
|
||||||
columns: List<MsColumnDefinition<T>>,
|
columns: List<MsColumnDefinition<T>>,
|
||||||
onRowClick: ((T) -> Unit)? = null,
|
onRowClick: ((T) -> Unit)? = null,
|
||||||
|
selectedItem: T? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
@@ -100,7 +101,12 @@ fun <T> MsDataTable(
|
|||||||
val state = androidx.compose.foundation.lazy.rememberLazyListState()
|
val state = androidx.compose.foundation.lazy.rememberLazyListState()
|
||||||
LazyColumn(state = state, modifier = Modifier.fillMaxSize()) {
|
LazyColumn(state = state, modifier = Modifier.fillMaxSize()) {
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index, item ->
|
||||||
val bgColor = if (index % 2 == 0) rowBackgroundColor else alternateRowBackgroundColor
|
val isSelected = item == selectedItem
|
||||||
|
val bgColor = when {
|
||||||
|
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
index % 2 == 0 -> rowBackgroundColor
|
||||||
|
else -> alternateRowBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = bgColor,
|
color = bgColor,
|
||||||
|
|||||||
+5
-4
@@ -1,5 +1,6 @@
|
|||||||
package at.mocode.frontend.features.funktionaer.di
|
package at.mocode.frontend.features.funktionaer.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
import at.mocode.frontend.features.funktionaer.presentation.*
|
import at.mocode.frontend.features.funktionaer.presentation.*
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
@@ -9,9 +10,9 @@ val funktionaerModule = module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockFunktionaerRepository : FunktionaerRepository {
|
class MockFunktionaerRepository : FunktionaerRepository {
|
||||||
override suspend fun list(): List<FunktionaerListItem> = listOf(
|
override suspend fun list(): List<Funktionaer> = listOf(
|
||||||
FunktionaerListItem(1, "Wolfgang Schier", "RICHTER", "G3"),
|
Funktionaer(1, "Wolfgang", "Schier", "12345", listOf("RICHTER"), "G3"),
|
||||||
FunktionaerListItem(2, "Alice Schwab", "RICHTER", "INTERNATIONAL"),
|
Funktionaer(2, "Alice", "Schwab", "23456", listOf("RICHTER"), "INTERNATIONAL"),
|
||||||
FunktionaerListItem(3, "Dietmar Gstöttner", "PARCOURSBAUER", null)
|
Funktionaer(3, "Dietmar", "Gstöttner", "34567", listOf("PARCOURSBAUER"), null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+209
-33
@@ -1,17 +1,20 @@
|
|||||||
package at.mocode.frontend.features.funktionaer.presentation
|
package at.mocode.frontend.features.funktionaer.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.Gavel
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FunktionaerScreen(
|
fun FunktionaerScreen(
|
||||||
@@ -24,19 +27,31 @@ fun FunktionaerScreen(
|
|||||||
FunktionaerListContent(
|
FunktionaerListContent(
|
||||||
state = state,
|
state = state,
|
||||||
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
|
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
|
||||||
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) }
|
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) },
|
||||||
|
onAddNew = { viewModel.send(FunktionaerIntent.AddNew) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
if (state.selectedId != null) {
|
if (state.isEditing) {
|
||||||
val selected = state.list.find { it.id == state.selectedId }
|
FunktionaerEditorContent(
|
||||||
if (selected != null) {
|
state = state,
|
||||||
FunktionaerDetailContent(selected)
|
onVornameChange = { viewModel.send(FunktionaerIntent.EditVorname(it)) },
|
||||||
}
|
onNachnameChange = { viewModel.send(FunktionaerIntent.EditNachname(it)) },
|
||||||
|
onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) },
|
||||||
|
onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) },
|
||||||
|
onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) },
|
||||||
|
onSave = { viewModel.send(FunktionaerIntent.Save) },
|
||||||
|
onCancel = { viewModel.send(FunktionaerIntent.Cancel) }
|
||||||
|
)
|
||||||
|
} else if (state.selectedFunktionaer != null) {
|
||||||
|
FunktionaerCard(
|
||||||
|
funktionaer = state.selectedFunktionaer!!,
|
||||||
|
onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaceholderContent(
|
PlaceholderContent(
|
||||||
title = "Kein Funktionär ausgewählt",
|
title = "Kein Funktionär ausgewählt",
|
||||||
subtitle = "Wählen Sie einen Funktionär aus der Liste aus."
|
subtitle = "Wählen Sie einen Richter oder Funktionär aus der Liste aus."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +62,21 @@ fun FunktionaerScreen(
|
|||||||
private fun FunktionaerListContent(
|
private fun FunktionaerListContent(
|
||||||
state: FunktionaerState,
|
state: FunktionaerState,
|
||||||
onSearchChange: (String) -> Unit,
|
onSearchChange: (String) -> Unit,
|
||||||
onFunktionaerSelected: (Long) -> Unit
|
onFunktionaerSelected: (Funktionaer) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
MsFilterBar(
|
MsFilterBar(
|
||||||
searchQuery = state.searchQuery,
|
searchQuery = state.searchQuery,
|
||||||
onSearchQueryChange = onSearchChange,
|
onSearchQueryChange = onSearchChange,
|
||||||
resultCount = state.filtered.size
|
resultCount = state.filtered.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
text = "Funktionär anlegen",
|
||||||
|
onClick = onAddNew,
|
||||||
|
icon = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -68,36 +91,189 @@ private fun FunktionaerListContent(
|
|||||||
columns = listOf(
|
columns = listOf(
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Name",
|
title = "Name",
|
||||||
|
weight = 1.5f,
|
||||||
|
cellRenderer = { Text("${it.vorname} ${it.nachname}", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Nr.",
|
||||||
|
width = 80.dp,
|
||||||
|
cellRenderer = { Text(it.richterNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Rollen",
|
||||||
weight = 1f,
|
weight = 1f,
|
||||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.rollen.joinToString(", "), style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
|
||||||
MsColumnDefinition(
|
|
||||||
title = "Rolle",
|
|
||||||
width = 150.dp,
|
|
||||||
cellRenderer = { Text(it.rolle, style = MaterialTheme.typography.bodySmall) }
|
|
||||||
),
|
|
||||||
MsColumnDefinition(
|
|
||||||
title = "Lizenz",
|
|
||||||
width = 100.dp,
|
|
||||||
cellRenderer = { Text(it.lizenz ?: "-", style = MaterialTheme.typography.bodySmall) }
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onRowClick = { onFunktionaerSelected(it.id) }
|
onRowClick = onFunktionaerSelected,
|
||||||
|
selectedItem = state.selectedFunktionaer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FunktionaerDetailContent(item: FunktionaerListItem) {
|
fun FunktionaerCard(
|
||||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
funktionaer: Funktionaer,
|
||||||
Text(item.name, style = MaterialTheme.typography.headlineMedium)
|
onEdit: () -> Unit
|
||||||
Spacer(Modifier.height(8.dp))
|
) {
|
||||||
Text("Rolle: ${item.rolle}", style = MaterialTheme.typography.bodyLarge)
|
Column(
|
||||||
item.lizenz?.let {
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
Text("Lizenz: $it", style = MaterialTheme.typography.bodyLarge)
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Gavel,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"${funktionaer.vorname} ${funktionaer.nachname}",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Richter-Nr: ${funktionaer.richterNummer ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsStatusBadge(
|
||||||
|
text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv",
|
||||||
|
containerColor = (if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error).copy(alpha = 0.1f),
|
||||||
|
contentColor = if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f))
|
||||||
|
FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.richterQualifikation ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
FunktionaerDetailItem(label = "E-Mail", value = funktionaer.email ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
FunktionaerDetailItem(label = "Telefon", value = funktionaer.telefon ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
text = "Daten bearbeiten",
|
||||||
|
onClick = onEdit,
|
||||||
|
fullWidth = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(24.dp))
|
}
|
||||||
Text("Weitere Details folgen in der nächsten Ausbaustufe.", style = MaterialTheme.typography.bodyMedium)
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FunktionaerEditorContent(
|
||||||
|
state: FunktionaerState,
|
||||||
|
onVornameChange: (String) -> Unit,
|
||||||
|
onNachnameChange: (String) -> Unit,
|
||||||
|
onRichterNummerChange: (String) -> Unit,
|
||||||
|
onEmailChange: (String) -> Unit,
|
||||||
|
onTelefonChange: (String) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsActionToolbar(
|
||||||
|
title = "Funktionär Details",
|
||||||
|
onSave = onSave,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = state.editVorname,
|
||||||
|
onValueChange = onVornameChange,
|
||||||
|
label = "Vorname",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = state.editNachname,
|
||||||
|
onValueChange = onNachnameChange,
|
||||||
|
label = "Nachname",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = state.editRichterNummer,
|
||||||
|
onValueChange = onRichterNummerChange,
|
||||||
|
label = "Richter-Nummer",
|
||||||
|
modifier = Modifier.width(300.dp),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = state.editEmail,
|
||||||
|
onValueChange = onEmailChange,
|
||||||
|
label = "E-Mail",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = state.editTelefon,
|
||||||
|
onValueChange = onTelefonChange,
|
||||||
|
label = "Telefon",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text("Zusätzliche Qualifikationen und Rollen werden über das ZNS-System synchronisiert.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-17
@@ -1,8 +1,8 @@
|
|||||||
package at.mocode.frontend.features.funktionaer.presentation
|
package at.mocode.frontend.features.funktionaer.presentation
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -17,9 +17,15 @@ data class FunktionaerListItem(
|
|||||||
data class FunktionaerState(
|
data class FunktionaerState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val list: List<FunktionaerListItem> = emptyList(),
|
val list: List<Funktionaer> = emptyList(),
|
||||||
val filtered: List<FunktionaerListItem> = emptyList(),
|
val filtered: List<Funktionaer> = emptyList(),
|
||||||
val selectedId: Long? = null,
|
val selectedFunktionaer: Funktionaer? = null,
|
||||||
|
val isEditing: Boolean = false,
|
||||||
|
val editVorname: String = "",
|
||||||
|
val editNachname: String = "",
|
||||||
|
val editRichterNummer: String = "",
|
||||||
|
val editEmail: String = "",
|
||||||
|
val editTelefon: String = "",
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,19 +33,25 @@ sealed interface FunktionaerIntent {
|
|||||||
data object Load : FunktionaerIntent
|
data object Load : FunktionaerIntent
|
||||||
data object Refresh : FunktionaerIntent
|
data object Refresh : FunktionaerIntent
|
||||||
data class SearchChanged(val query: String) : FunktionaerIntent
|
data class SearchChanged(val query: String) : FunktionaerIntent
|
||||||
data class Select(val id: Long?) : FunktionaerIntent
|
data class Select(val funktionaer: Funktionaer?) : FunktionaerIntent
|
||||||
|
data object AddNew : FunktionaerIntent
|
||||||
|
data class EditVorname(val value: String) : FunktionaerIntent
|
||||||
|
data class EditNachname(val value: String) : FunktionaerIntent
|
||||||
|
data class EditRichterNummer(val value: String) : FunktionaerIntent
|
||||||
|
data class EditEmail(val value: String) : FunktionaerIntent
|
||||||
|
data class EditTelefon(val value: String) : FunktionaerIntent
|
||||||
|
data object Save : FunktionaerIntent
|
||||||
|
data object Cancel : FunktionaerIntent
|
||||||
data object ClearError : FunktionaerIntent
|
data object ClearError : FunktionaerIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FunktionaerRepository {
|
interface FunktionaerRepository {
|
||||||
suspend fun list(): List<FunktionaerListItem>
|
suspend fun list(): List<Funktionaer>
|
||||||
}
|
}
|
||||||
|
|
||||||
class FunktionaerViewModel(
|
class FunktionaerViewModel(
|
||||||
private val repo: FunktionaerRepository,
|
private val repo: FunktionaerRepository,
|
||||||
) {
|
) : ViewModel() {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
|
private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
|
||||||
val state: StateFlow<FunktionaerState> = _state
|
val state: StateFlow<FunktionaerState> = _state
|
||||||
|
|
||||||
@@ -49,14 +61,44 @@ class FunktionaerViewModel(
|
|||||||
when (intent) {
|
when (intent) {
|
||||||
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
|
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
|
||||||
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
||||||
is FunktionaerIntent.Select -> reduce { it.copy(selectedId = intent.id) }
|
is FunktionaerIntent.Select -> reduce {
|
||||||
|
it.copy(
|
||||||
|
selectedFunktionaer = intent.funktionaer,
|
||||||
|
isEditing = intent.funktionaer != null,
|
||||||
|
editVorname = intent.funktionaer?.vorname ?: "",
|
||||||
|
editNachname = intent.funktionaer?.nachname ?: "",
|
||||||
|
editRichterNummer = intent.funktionaer?.richterNummer ?: "",
|
||||||
|
editEmail = intent.funktionaer?.email ?: "",
|
||||||
|
editTelefon = intent.funktionaer?.telefon ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FunktionaerIntent.AddNew -> reduce {
|
||||||
|
it.copy(
|
||||||
|
selectedFunktionaer = null,
|
||||||
|
isEditing = true,
|
||||||
|
editVorname = "",
|
||||||
|
editNachname = "",
|
||||||
|
editRichterNummer = "",
|
||||||
|
editEmail = "",
|
||||||
|
editTelefon = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FunktionaerIntent.EditVorname -> reduce { it.copy(editVorname = intent.value) }
|
||||||
|
is FunktionaerIntent.EditNachname -> reduce { it.copy(editNachname = intent.value) }
|
||||||
|
is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) }
|
||||||
|
is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) }
|
||||||
|
is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) }
|
||||||
|
is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) }
|
||||||
|
is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) }
|
||||||
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
reduce { it.copy(isLoading = true, errorMessage = null) }
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
scope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val items = repo.list()
|
val items = repo.list()
|
||||||
reduce { cur ->
|
reduce { cur ->
|
||||||
@@ -75,13 +117,13 @@ class FunktionaerViewModel(
|
|||||||
reduce { it.copy(filtered = filtered) }
|
reduce { it.copy(filtered = filtered) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterList(list: List<FunktionaerListItem>, query: String): List<FunktionaerListItem> {
|
private fun filterList(list: List<Funktionaer>, query: String): List<Funktionaer> {
|
||||||
if (query.isBlank()) return list
|
if (query.isBlank()) return list
|
||||||
val q = query.trim()
|
val q = query.trim()
|
||||||
return list.filter {
|
return list.filter {
|
||||||
it.name.contains(q, ignoreCase = true) ||
|
it.vorname.contains(q, ignoreCase = true) ||
|
||||||
it.rolle.contains(q, ignoreCase = true) ||
|
it.nachname.contains(q, ignoreCase = true) ||
|
||||||
(it.lizenz?.contains(q, ignoreCase = true) ?: false)
|
(it.richterNummer?.contains(q, ignoreCase = true) ?: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+144
-17
@@ -1,11 +1,14 @@
|
|||||||
package at.mocode.frontend.features.pferde.presentation
|
package at.mocode.frontend.features.pferde.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.Pets
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
@@ -24,7 +27,8 @@ fun PferdeScreen(
|
|||||||
PferdeListContent(
|
PferdeListContent(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onSearchChange = viewModel::onSearchQueryChange,
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
onPferdSelected = viewModel::selectPferd
|
onPferdSelected = viewModel::selectPferd,
|
||||||
|
onAddNew = { viewModel.addNewPferd() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
@@ -43,6 +47,11 @@ fun PferdeScreen(
|
|||||||
onSave = viewModel::onSave,
|
onSave = viewModel::onSave,
|
||||||
onCancel = viewModel::onCancel
|
onCancel = viewModel::onCancel
|
||||||
)
|
)
|
||||||
|
} else if (uiState.selectedPferd != null) {
|
||||||
|
PferdCard(
|
||||||
|
pferd = uiState.selectedPferd,
|
||||||
|
onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaceholderContent(
|
PlaceholderContent(
|
||||||
title = "Kein Pferd ausgewählt",
|
title = "Kein Pferd ausgewählt",
|
||||||
@@ -57,13 +66,21 @@ fun PferdeScreen(
|
|||||||
private fun PferdeListContent(
|
private fun PferdeListContent(
|
||||||
uiState: PferdeUiState,
|
uiState: PferdeUiState,
|
||||||
onSearchChange: (String) -> Unit,
|
onSearchChange: (String) -> Unit,
|
||||||
onPferdSelected: (Pferd) -> Unit
|
onPferdSelected: (Pferd) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
MsFilterBar(
|
MsFilterBar(
|
||||||
searchQuery = uiState.searchQuery,
|
searchQuery = uiState.searchQuery,
|
||||||
onSearchQueryChange = onSearchChange,
|
onSearchQueryChange = onSearchChange,
|
||||||
resultCount = uiState.searchResults.size
|
resultCount = uiState.searchResults.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
onClick = onAddNew,
|
||||||
|
text = "Pferd anlegen",
|
||||||
|
icon = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -77,9 +94,9 @@ private fun PferdeListContent(
|
|||||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Lebensnummer",
|
title = "ÖPS-Nr.",
|
||||||
width = 150.dp,
|
width = 100.dp,
|
||||||
cellRenderer = { Text(it.lebensnummer, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.oepsNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Status",
|
title = "Status",
|
||||||
@@ -93,11 +110,114 @@ private fun PferdeListContent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onRowClick = onPferdSelected
|
onRowClick = onPferdSelected,
|
||||||
|
selectedItem = uiState.selectedPferd
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PferdCard(
|
||||||
|
pferd: Pferd,
|
||||||
|
onEdit: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Pets,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
pferd.name,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
pferd.lebensnummer,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsStatusBadge(
|
||||||
|
text = pferd.status.label,
|
||||||
|
containerColor = pferd.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = pferd.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DetailItem(label = "Geschlecht", value = pferd.geschlecht.label, modifier = Modifier.weight(1f))
|
||||||
|
DetailItem(label = "Farbe", value = pferd.farbe, modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
onClick = onEdit,
|
||||||
|
text = "Pferdedaten bearbeiten",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PferdeEditorContent(
|
private fun PferdeEditorContent(
|
||||||
uiState: PferdeUiState,
|
uiState: PferdeUiState,
|
||||||
@@ -127,13 +247,15 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editName,
|
value = uiState.editName,
|
||||||
onValueChange = onNameChange,
|
onValueChange = onNameChange,
|
||||||
label = "Name",
|
label = "Name",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editLebensnummer,
|
value = uiState.editLebensnummer,
|
||||||
onValueChange = onLebensnummerChange,
|
onValueChange = onLebensnummerChange,
|
||||||
label = "Lebensnummer",
|
label = "Lebensnummer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +266,15 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editFeiId,
|
value = uiState.editFeiId,
|
||||||
onValueChange = onFeiIdChange,
|
onValueChange = onFeiIdChange,
|
||||||
label = "FEI ID",
|
label = "FEI ID",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editOepsNummer,
|
value = uiState.editOepsNummer,
|
||||||
onValueChange = onOepsNummerChange,
|
onValueChange = onOepsNummerChange,
|
||||||
label = "ÖPS Nummer",
|
label = "ÖPS Nummer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +293,8 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editFarbe,
|
value = uiState.editFarbe,
|
||||||
onValueChange = onFarbeChange,
|
onValueChange = onFarbeChange,
|
||||||
label = "Farbe",
|
label = "Farbe",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +305,15 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editGeburtsjahr,
|
value = uiState.editGeburtsjahr,
|
||||||
onValueChange = onGeburtsjahrChange,
|
onValueChange = onGeburtsjahrChange,
|
||||||
label = "Geburtsjahr",
|
label = "Geburtsjahr",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editBesitzer,
|
value = uiState.editBesitzer,
|
||||||
onValueChange = onBesitzerChange,
|
onValueChange = onBesitzerChange,
|
||||||
label = "Besitzer",
|
label = "Besitzer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -17,6 +17,7 @@ data class PferdeUiState(
|
|||||||
val selectedPferd: Pferd? = null,
|
val selectedPferd: Pferd? = null,
|
||||||
val isEditing: Boolean = false,
|
val isEditing: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val editId: String = "",
|
||||||
val editName: String = "",
|
val editName: String = "",
|
||||||
val editLebensnummer: String = "",
|
val editLebensnummer: String = "",
|
||||||
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||||
@@ -59,6 +60,7 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
uiState = uiState.copy(
|
uiState = uiState.copy(
|
||||||
selectedPferd = pferd,
|
selectedPferd = pferd,
|
||||||
isEditing = true,
|
isEditing = true,
|
||||||
|
editId = pferd.id,
|
||||||
editName = pferd.name,
|
editName = pferd.name,
|
||||||
editLebensnummer = pferd.lebensnummer,
|
editLebensnummer = pferd.lebensnummer,
|
||||||
editGeschlecht = pferd.geschlecht,
|
editGeschlecht = pferd.geschlecht,
|
||||||
@@ -71,6 +73,23 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addNewPferd() {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedPferd = null,
|
||||||
|
isEditing = true,
|
||||||
|
editId = "",
|
||||||
|
editName = "",
|
||||||
|
editLebensnummer = "",
|
||||||
|
editGeschlecht = Geschlecht.WALLACH,
|
||||||
|
editFarbe = "",
|
||||||
|
editGeburtsjahr = "",
|
||||||
|
editStatus = PferdeStatus.AKTIV,
|
||||||
|
editFeiId = "",
|
||||||
|
editOepsNummer = "",
|
||||||
|
editBesitzer = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onEditFeiIdChange(value: String) {
|
fun onEditFeiIdChange(value: String) {
|
||||||
uiState = uiState.copy(editFeiId = value)
|
uiState = uiState.copy(editFeiId = value)
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-21
@@ -1,9 +1,9 @@
|
|||||||
package at.mocode.frontend.features.reiter.presentation
|
package at.mocode.frontend.features.reiter.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
@@ -23,7 +23,8 @@ fun ReiterScreen(
|
|||||||
ReiterListContent(
|
ReiterListContent(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onSearchChange = viewModel::onSearchQueryChange,
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
onReiterSelected = viewModel::selectReiter
|
onReiterSelected = viewModel::selectReiter,
|
||||||
|
onAddNew = { viewModel.addNewReiter() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
@@ -43,6 +44,11 @@ fun ReiterScreen(
|
|||||||
onSave = viewModel::onSave,
|
onSave = viewModel::onSave,
|
||||||
onCancel = viewModel::onCancel
|
onCancel = viewModel::onCancel
|
||||||
)
|
)
|
||||||
|
} else if (uiState.selectedReiter != null) {
|
||||||
|
ReiterCard(
|
||||||
|
reiter = uiState.selectedReiter,
|
||||||
|
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaceholderContent(
|
PlaceholderContent(
|
||||||
title = "Kein Reiter ausgewählt",
|
title = "Kein Reiter ausgewählt",
|
||||||
@@ -57,13 +63,20 @@ fun ReiterScreen(
|
|||||||
private fun ReiterListContent(
|
private fun ReiterListContent(
|
||||||
uiState: ReiterUiState,
|
uiState: ReiterUiState,
|
||||||
onSearchChange: (String) -> Unit,
|
onSearchChange: (String) -> Unit,
|
||||||
onReiterSelected: (Reiter) -> Unit
|
onReiterSelected: (Reiter) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
MsFilterBar(
|
MsFilterBar(
|
||||||
searchQuery = uiState.searchQuery,
|
searchQuery = uiState.searchQuery,
|
||||||
onSearchQueryChange = onSearchChange,
|
onSearchQueryChange = onSearchChange,
|
||||||
resultCount = uiState.searchResults.size
|
resultCount = uiState.searchResults.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
text = "Reiter anlegen",
|
||||||
|
onClick = onAddNew
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -72,14 +85,9 @@ private fun ReiterListContent(
|
|||||||
items = uiState.searchResults,
|
items = uiState.searchResults,
|
||||||
columns = listOf(
|
columns = listOf(
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Vorname",
|
title = "Name",
|
||||||
weight = 1f,
|
weight = 1.5f,
|
||||||
cellRenderer = { Text(it.vorname, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
|
||||||
MsColumnDefinition(
|
|
||||||
title = "Nachname",
|
|
||||||
weight = 1f,
|
|
||||||
cellRenderer = { Text(it.nachname, style = MaterialTheme.typography.bodySmall) }
|
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Lizenz",
|
title = "Lizenz",
|
||||||
@@ -103,6 +111,107 @@ private fun ReiterListContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReiterCard(
|
||||||
|
reiter: Reiter,
|
||||||
|
onEdit: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = (reiter.vorname.take(1) + reiter.nachname.take(1)).uppercase(),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
reiter.name,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"ÖPS-Nr: ${reiter.oepsNummer ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsStatusBadge(
|
||||||
|
text = reiter.status.label,
|
||||||
|
containerColor = reiter.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = reiter.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f))
|
||||||
|
ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ReiterDetailItem(label = "E-Mail", value = reiter.email ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
ReiterDetailItem(label = "Telefon", value = reiter.telefon ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ReiterDetailItem(label = "Verein", value = reiter.verein ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
text = "Reiterdaten bearbeiten",
|
||||||
|
onClick = onEdit,
|
||||||
|
fullWidth = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReiterEditorContent(
|
private fun ReiterEditorContent(
|
||||||
uiState: ReiterUiState,
|
uiState: ReiterUiState,
|
||||||
@@ -133,13 +242,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editVorname,
|
value = uiState.editVorname,
|
||||||
onValueChange = onVornameChange,
|
onValueChange = onVornameChange,
|
||||||
label = "Vorname",
|
label = "Vorname",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editName,
|
value = uiState.editName,
|
||||||
onValueChange = onNachnameChange,
|
onValueChange = onNachnameChange,
|
||||||
label = "Nachname",
|
label = "Nachname",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,13 +261,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editFeiId,
|
value = uiState.editFeiId,
|
||||||
onValueChange = onFeiIdChange,
|
onValueChange = onFeiIdChange,
|
||||||
label = "FEI ID",
|
label = "FEI ID",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editOepsNummer,
|
value = uiState.editOepsNummer,
|
||||||
onValueChange = onOepsNummerChange,
|
onValueChange = onOepsNummerChange,
|
||||||
label = "ÖPS Nummer",
|
label = "ÖPS Nummer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,13 +280,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editGeburtsdatum,
|
value = uiState.editGeburtsdatum,
|
||||||
onValueChange = onGeburtsdatumChange,
|
onValueChange = onGeburtsdatumChange,
|
||||||
label = "Geburtsdatum",
|
label = "Geburtsdatum",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editVerein,
|
value = uiState.editVerein,
|
||||||
onValueChange = onVereinChange,
|
onValueChange = onVereinChange,
|
||||||
label = "Verein",
|
label = "Verein",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,13 +299,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editEmail,
|
value = uiState.editEmail,
|
||||||
onValueChange = onEmailChange,
|
onValueChange = onEmailChange,
|
||||||
label = "E-Mail",
|
label = "E-Mail",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editTelefon,
|
value = uiState.editTelefon,
|
||||||
onValueChange = onTelefonChange,
|
onValueChange = onTelefonChange,
|
||||||
label = "Telefon",
|
label = "Telefon",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
@@ -18,6 +18,7 @@ data class ReiterUiState(
|
|||||||
val selectedReiter: Reiter? = null,
|
val selectedReiter: Reiter? = null,
|
||||||
val isEditing: Boolean = false,
|
val isEditing: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val editId: String = "",
|
||||||
val editName: String = "",
|
val editName: String = "",
|
||||||
val editVorname: String = "",
|
val editVorname: String = "",
|
||||||
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||||
@@ -65,6 +66,7 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
uiState = uiState.copy(
|
uiState = uiState.copy(
|
||||||
selectedReiter = reiter,
|
selectedReiter = reiter,
|
||||||
isEditing = true,
|
isEditing = true,
|
||||||
|
editId = reiter.id,
|
||||||
editVorname = reiter.vorname,
|
editVorname = reiter.vorname,
|
||||||
editName = reiter.nachname,
|
editName = reiter.nachname,
|
||||||
editLizenz = reiter.lizenz,
|
editLizenz = reiter.lizenz,
|
||||||
@@ -79,6 +81,25 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addNewReiter() {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedReiter = null,
|
||||||
|
isEditing = true,
|
||||||
|
editId = "",
|
||||||
|
editVorname = "",
|
||||||
|
editName = "",
|
||||||
|
editLizenz = LizenzKlasse.KEINE,
|
||||||
|
editSparte = Sparte.KEINE,
|
||||||
|
editStatus = ReiterStatus.AKTIV,
|
||||||
|
editFeiId = "",
|
||||||
|
editOepsNummer = "",
|
||||||
|
editGeburtsdatum = "",
|
||||||
|
editEmail = "",
|
||||||
|
editTelefon = "",
|
||||||
|
editVerein = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
|
fun onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
|
||||||
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
|
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
|
||||||
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }
|
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"deviceName": "Meldestelle",
|
"deviceName": "Meldestelle",
|
||||||
"sharedKey": "Paassword",
|
"sharedKey": "Password",
|
||||||
"backupPath": "/mocode/meldestelle/docs/temp",
|
"backupPath": "/mocode/meldestelle/docs/temp",
|
||||||
"networkRole": "MASTER",
|
"networkRole": "MASTER",
|
||||||
"expectedClients": [
|
"expectedClients": [
|
||||||
|
|||||||
+2
@@ -12,6 +12,7 @@ import at.mocode.frontend.core.network.networkModule
|
|||||||
import at.mocode.frontend.core.sync.di.syncModule
|
import at.mocode.frontend.core.sync.di.syncModule
|
||||||
import at.mocode.frontend.features.billing.di.billingModule
|
import at.mocode.frontend.features.billing.di.billingModule
|
||||||
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
||||||
|
import at.mocode.frontend.features.funktionaer.di.funktionaerModule
|
||||||
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
|
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
|
||||||
import at.mocode.frontend.features.pferde.di.pferdeModule
|
import at.mocode.frontend.features.pferde.di.pferdeModule
|
||||||
import at.mocode.frontend.features.profile.di.profileModule
|
import at.mocode.frontend.features.profile.di.profileModule
|
||||||
@@ -42,6 +43,7 @@ fun main() = application {
|
|||||||
billingModule,
|
billingModule,
|
||||||
pferdeModule,
|
pferdeModule,
|
||||||
reiterModule,
|
reiterModule,
|
||||||
|
funktionaerModule,
|
||||||
vereinFeatureModule,
|
vereinFeatureModule,
|
||||||
turnierFeatureModule,
|
turnierFeatureModule,
|
||||||
deviceInitializationModule,
|
deviceInitializationModule,
|
||||||
|
|||||||
+76
-15
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||||
@@ -29,6 +30,9 @@ import at.mocode.frontend.features.device.initialization.data.local.DeviceInitia
|
|||||||
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||||
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen
|
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen
|
||||||
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
|
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerIntent
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerScreen
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerViewModel
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
|
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||||
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
||||||
@@ -176,12 +180,64 @@ private fun DesktopNavRail(
|
|||||||
)
|
)
|
||||||
|
|
||||||
NavRailItem(
|
NavRailItem(
|
||||||
icon = Icons.Default.People,
|
icon = Icons.Default.CloudDownload,
|
||||||
label = "Vereine",
|
label = "ZNS-Import",
|
||||||
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung,
|
selected = currentScreen is AppScreen.StammdatenImport,
|
||||||
onClick = { onNavigate(AppScreen.Vereine) }
|
onClick = { onNavigate(AppScreen.StammdatenImport) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var showStammdatenMenu by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
label = "Stammdaten",
|
||||||
|
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung ||
|
||||||
|
currentScreen is AppScreen.Reiter || currentScreen is AppScreen.ReiterVerwaltung ||
|
||||||
|
currentScreen is AppScreen.Pferde || currentScreen is AppScreen.PferdVerwaltung ||
|
||||||
|
currentScreen is AppScreen.FunktionaerVerwaltung,
|
||||||
|
onClick = { showStammdatenMenu = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showStammdatenMenu,
|
||||||
|
onDismissRequest = { showStammdatenMenu = false },
|
||||||
|
offset = DpOffset(Dimens.NavRailWidth, 0.dp)
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Vereine") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Vereine)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.People, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Reiter") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Reiter)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Pferde") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Pferde)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Richter") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.FunktionaerVerwaltung)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Gavel, contentDescription = null) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavRailItem(
|
NavRailItem(
|
||||||
icon = Icons.Default.Email,
|
icon = Icons.Default.Email,
|
||||||
label = "Mails",
|
label = "Mails",
|
||||||
@@ -573,7 +629,7 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Pferde-Verwaltung & Profil ---
|
// --- Pferde-Verwaltung & Profil ---
|
||||||
is AppScreen.PferdVerwaltung -> {
|
is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
|
||||||
val viewModel = koinViewModel<PferdeViewModel>()
|
val viewModel = koinViewModel<PferdeViewModel>()
|
||||||
PferdeScreen(viewModel = viewModel)
|
PferdeScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
@@ -591,7 +647,7 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Reiter-Verwaltung & Profil ---
|
// --- Reiter-Verwaltung & Profil ---
|
||||||
is AppScreen.ReiterVerwaltung -> {
|
is AppScreen.Reiter, is AppScreen.ReiterVerwaltung -> {
|
||||||
val viewModel = koinViewModel<ReiterViewModel>()
|
val viewModel = koinViewModel<ReiterViewModel>()
|
||||||
ReiterScreen(viewModel = viewModel)
|
ReiterScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
@@ -607,7 +663,7 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Verein-Verwaltung & Profil ---
|
// --- Verein-Verwaltung & Profil ---
|
||||||
is AppScreen.VereinVerwaltung -> {
|
is AppScreen.Vereine, is AppScreen.VereinVerwaltung -> {
|
||||||
println("[Screen] Rendering VereinVerwaltung (VereinScreen)")
|
println("[Screen] Rendering VereinVerwaltung (VereinScreen)")
|
||||||
val vereinViewModel: VereinViewModel = koinViewModel()
|
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||||
VereinScreen(viewModel = vereinViewModel)
|
VereinScreen(viewModel = vereinViewModel)
|
||||||
@@ -621,15 +677,20 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Funktionaer-Verwaltung & Profil ---
|
// --- Funktionaer-Verwaltung & Profil ---
|
||||||
is AppScreen.FunktionaerVerwaltung -> FunktionaerVerwaltungScreen(
|
is AppScreen.FunktionaerVerwaltung -> {
|
||||||
onBack = onBack,
|
val viewModel = koinViewModel<FunktionaerViewModel>()
|
||||||
onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) }
|
FunktionaerScreen(viewModel = viewModel)
|
||||||
)
|
}
|
||||||
|
|
||||||
is AppScreen.FunktionaerProfil -> FunktionaerProfil(
|
is AppScreen.FunktionaerProfil -> {
|
||||||
id = currentScreen.id,
|
val viewModel = koinViewModel<FunktionaerViewModel>()
|
||||||
onBack = onBack,
|
LaunchedEffect(currentScreen.id) {
|
||||||
)
|
viewModel.state.value.list.find { it.id == currentScreen.id }?.let {
|
||||||
|
viewModel.send(FunktionaerIntent.Select(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FunktionaerScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Veranstalter-Verwaltung & Profil ---
|
// --- Veranstalter-Verwaltung & Profil ---
|
||||||
is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
|
is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ dev.port.offset=0
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
|
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
|
||||||
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
|
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
|
||||||
enableWasm=false
|
enableWasm=true
|
||||||
|
|
||||||
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
||||||
# See https://kotl.in/dokka-gradle-migration
|
# See https://kotl.in/dokka-gradle-migration
|
||||||
|
|||||||
Reference in New Issue
Block a user