feat(pferde-feature): introduce Pferde management module with screens, ViewModel, and domain models
- Added `pferde-feature` module for managing horses, including list, detail, and editing views. - Implemented `MsMasterDetailLayout` for PferdeScreen, integrating `MsDataTable`, `MsFilterBar`, and `MsActionToolbar`. - Defined domain models (`Pferd`, `Geschlecht`, `PferdeStatus`) with mock data support. - Updated roadmap to mark `Pferde-Verwaltung (MVP)` as complete. - Registered the new module in `settings.gradle.kts` and `meldestelle-desktop` build configuration. - Added previews for Pferde and Reiter components to support IDE render. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Feature-Modul: Pferde-Verwaltung (Desktop-only)
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
sourceSets {
|
||||
jvmMain.dependencies {
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package at.mocode.frontend.features.pferde.domain
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* UI-Modell für ein Pferd.
|
||||
*/
|
||||
data class Pferd(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lebensnummer: String,
|
||||
val geschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||
val farbe: String = "",
|
||||
val geburtsjahr: Int? = null,
|
||||
val status: PferdeStatus = PferdeStatus.AKTIV
|
||||
)
|
||||
|
||||
enum class Geschlecht(val label: String) {
|
||||
WALLACH("Wallach"),
|
||||
STUTE("Stute"),
|
||||
HENGST("Hengst")
|
||||
}
|
||||
|
||||
enum class PferdeStatus(val label: String, val color: Color) {
|
||||
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||
INAKTIV("Inaktiv", Color(0xFF757575)),
|
||||
GESTOKEN("Gestorben", Color(0xFFC62828)),
|
||||
VERKAUFT("Verkauft", Color(0xFF0277BD))
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
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.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.*
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
import at.mocode.frontend.features.pferde.domain.Geschlecht
|
||||
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||
|
||||
@Composable
|
||||
fun PferdeScreen(
|
||||
viewModel: PferdeViewModel = PferdeViewModel()
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
|
||||
MsMasterDetailLayout(
|
||||
master = {
|
||||
PferdeListContent(
|
||||
uiState = uiState,
|
||||
onSearchChange = viewModel::onSearchQueryChange,
|
||||
onPferdSelected = viewModel::selectPferd
|
||||
)
|
||||
},
|
||||
detail = {
|
||||
if (uiState.isEditing) {
|
||||
PferdeEditorContent(
|
||||
uiState = uiState,
|
||||
onNameChange = viewModel::onEditNameChange,
|
||||
onLebensnummerChange = viewModel::onEditLebensnummerChange,
|
||||
onGeschlechtChange = viewModel::onEditGeschlechtChange,
|
||||
onFarbeChange = viewModel::onEditFarbeChange,
|
||||
onGeburtsjahrChange = viewModel::onEditGeburtsjahrChange,
|
||||
onStatusChange = viewModel::onEditStatusChange,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Pferd ausgewählt",
|
||||
subtitle = "Wählen Sie ein Pferd aus der Liste aus oder legen Sie ein neues an."
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PferdeListContent(
|
||||
uiState: PferdeUiState,
|
||||
onSearchChange: (String) -> Unit,
|
||||
onPferdSelected: (Pferd) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
MsFilterBar(
|
||||
searchQuery = uiState.searchQuery,
|
||||
onSearchQueryChange = onSearchChange,
|
||||
resultCount = uiState.searchResults.size
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
MsDataTable(
|
||||
items = uiState.searchResults,
|
||||
columns = listOf(
|
||||
MsColumnDefinition(
|
||||
title = "Name",
|
||||
weight = 1f,
|
||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Lebensnummer",
|
||||
width = 150.dp,
|
||||
cellRenderer = { Text(it.lebensnummer, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Status",
|
||||
width = 100.dp,
|
||||
cellRenderer = {
|
||||
MsStatusBadge(
|
||||
text = it.status.label,
|
||||
containerColor = it.status.color.copy(alpha = 0.1f),
|
||||
contentColor = it.status.color
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
onRowClick = onPferdSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PferdeEditorContent(
|
||||
uiState: PferdeUiState,
|
||||
onNameChange: (String) -> Unit,
|
||||
onLebensnummerChange: (String) -> Unit,
|
||||
onGeschlechtChange: (Geschlecht) -> Unit,
|
||||
onFarbeChange: (String) -> Unit,
|
||||
onGeburtsjahrChange: (String) -> Unit,
|
||||
onStatusChange: (PferdeStatus) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
MsActionToolbar(
|
||||
title = "Pferde Details",
|
||||
onSave = onSave,
|
||||
onCancel = onCancel
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editName,
|
||||
onValueChange = onNameChange,
|
||||
label = "Name",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editLebensnummer,
|
||||
onValueChange = onLebensnummerChange,
|
||||
label = "Lebensnummer",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsEnumDropdown(
|
||||
label = "Geschlecht",
|
||||
options = Geschlecht.entries.toTypedArray(),
|
||||
selectedOption = uiState.editGeschlecht,
|
||||
onOptionSelected = onGeschlechtChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editFarbe,
|
||||
onValueChange = onFarbeChange,
|
||||
label = "Farbe",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editGeburtsjahr,
|
||||
onValueChange = onGeburtsjahrChange,
|
||||
label = "Geburtsjahr",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsEnumDropdown(
|
||||
label = "Status",
|
||||
options = PferdeStatus.entries.toTypedArray(),
|
||||
selectedOption = uiState.editStatus,
|
||||
onOptionSelected = onStatusChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
if (uiState.editStatus == PferdeStatus.INAKTIV) {
|
||||
MsValidationWrapper(
|
||||
messages = listOf(
|
||||
ValidationMessage(
|
||||
"Pferd ist als inaktiv markiert und kann nicht für Nennungen verwendet werden.",
|
||||
ValidationSeverity.WARNING
|
||||
)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
"Zusätzliche Pferde-Informationen",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-Place Preview für den PferdeScreen.
|
||||
*/
|
||||
@Composable
|
||||
fun PferdeScreenPreviewContent() {
|
||||
val viewModel = PferdeViewModel()
|
||||
at.mocode.frontend.core.designsystem.theme.AppTheme {
|
||||
Surface {
|
||||
PferdeScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package at.mocode.frontend.features.pferde.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import at.mocode.frontend.features.pferde.domain.Geschlecht
|
||||
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||
|
||||
/**
|
||||
* UI-State für die Pferde-Verwaltung.
|
||||
*/
|
||||
data class PferdeUiState(
|
||||
val searchResults: List<Pferd> = emptyList(),
|
||||
val searchQuery: String = "",
|
||||
val selectedPferd: Pferd? = null,
|
||||
val isEditing: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val editName: String = "",
|
||||
val editLebensnummer: String = "",
|
||||
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||
val editFarbe: String = "",
|
||||
val editGeburtsjahr: String = "",
|
||||
val editStatus: PferdeStatus = PferdeStatus.AKTIV
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel für die Pferde-Verwaltung.
|
||||
*/
|
||||
open class PferdeViewModel(initialLoad: Boolean = true) {
|
||||
var uiState by mutableStateOf(PferdeUiState())
|
||||
protected set
|
||||
|
||||
init {
|
||||
if (initialLoad) {
|
||||
loadPferde()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPferde() {
|
||||
val mockData = listOf(
|
||||
Pferd("1", "Bella", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
||||
Pferd("2", "Casanova", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
||||
Pferd("3", "Spirit", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
||||
Pferd("4", "Lucky", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
||||
)
|
||||
uiState = uiState.copy(searchResults = mockData)
|
||||
}
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
uiState = uiState.copy(searchQuery = query)
|
||||
}
|
||||
|
||||
fun selectPferd(pferd: Pferd) {
|
||||
uiState = uiState.copy(
|
||||
selectedPferd = pferd,
|
||||
isEditing = true,
|
||||
editName = pferd.name,
|
||||
editLebensnummer = pferd.lebensnummer,
|
||||
editGeschlecht = pferd.geschlecht,
|
||||
editFarbe = pferd.farbe,
|
||||
editGeburtsjahr = pferd.geburtsjahr?.toString() ?: "",
|
||||
editStatus = pferd.status
|
||||
)
|
||||
}
|
||||
|
||||
fun onEditNameChange(value: String) {
|
||||
uiState = uiState.copy(editName = value)
|
||||
}
|
||||
|
||||
fun onEditLebensnummerChange(value: String) {
|
||||
uiState = uiState.copy(editLebensnummer = value)
|
||||
}
|
||||
|
||||
fun onEditGeschlechtChange(value: Geschlecht) {
|
||||
uiState = uiState.copy(editGeschlecht = value)
|
||||
}
|
||||
|
||||
fun onEditFarbeChange(value: String) {
|
||||
uiState = uiState.copy(editFarbe = value)
|
||||
}
|
||||
|
||||
fun onEditGeburtsjahrChange(value: String) {
|
||||
uiState = uiState.copy(editGeburtsjahr = value)
|
||||
}
|
||||
|
||||
fun onEditStatusChange(value: PferdeStatus) {
|
||||
uiState = uiState.copy(editStatus = value)
|
||||
}
|
||||
|
||||
fun onSave() {
|
||||
uiState = uiState.copy(isEditing = false)
|
||||
}
|
||||
|
||||
fun onCancel() {
|
||||
uiState = uiState.copy(isEditing = false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user