feat(reiter-feature): introduce Reiter management module with screens, ViewModel, and domain models
- Added `reiter-feature` module for managing riders, including list and detail views. - Implemented `MsMasterDetailLayout` for ReiterScreen, integrating `MsDataTable`, `MsFilterBar`, and `MsActionToolbar`. - Defined domain models (`Reiter`, `LizenzKlasse`, `Sparte`, `ReiterStatus`) with mock data support. - Updated roadmap to reflect progress in Phase 5: Routing & Screen-Komposition. - Registered the new module in `settings.gradle.kts`. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
parent
659e699c33
commit
94306329c9
|
|
@ -47,7 +47,7 @@ Eingabe von Stammdaten muss schnell und fehlerfrei erfolgen.
|
|||
* [x] **`MsValidationWrapper`:** Konsistente Anzeige von Fehlern und Warnungen (z.B. ÖTO-Validierungsregeln).
|
||||
* [x] **`MsSearchableSelect`:** Für die Verknüpfung von Reitern/Pferden (Autocomplete-Suche).
|
||||
|
||||
## Phase 4: Layout-Patterns & Navigation 🔵 [IN ARBEIT]
|
||||
## Phase 4: Layout-Patterns & Navigation ✅ [ABGESCHLOSSEN]
|
||||
|
||||
Hier bringen wir alles zusammen, bevor das finale Routing implementiert wird.
|
||||
|
||||
|
|
@ -57,6 +57,16 @@ Hier bringen wir alles zusammen, bevor das finale Routing implementiert wird.
|
|||
|
||||
---
|
||||
|
||||
## Phase 5: Routing & Screen-Komposition 🔵 [IN ARBEIT]
|
||||
|
||||
In dieser Phase werden die Komponenten zu echten Features zusammengebaut.
|
||||
|
||||
* [x] **Reiter-Verwaltung (MVP):** Erster Screen mit `MsMasterDetailLayout`, `MsDataTable` und Editor.
|
||||
* [ ] **Pferde-Verwaltung (MVP):** Analog zur Reiter-Verwaltung.
|
||||
* [ ] **Navigation & Routing:** Integration der neuen Screens in die Hauptnavigation.
|
||||
|
||||
---
|
||||
|
||||
## Erfolgs-Metriken
|
||||
|
||||
* **Wiederverwendbarkeit:** > 80% der UI besteht aus `Ms`-Komponenten.
|
||||
|
|
|
|||
30
frontend/features/reiter-feature/build.gradle.kts
Normal file
30
frontend/features/reiter-feature/build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Feature-Modul: Reiter-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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package at.mocode.frontend.features.reiter.domain
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* UI-Modell für einen Reiter.
|
||||
*/
|
||||
data class Reiter(
|
||||
val id: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val satznummer: String?,
|
||||
val lizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||
val sparte: Sparte = Sparte.KEINE,
|
||||
val status: ReiterStatus = ReiterStatus.AKTIV
|
||||
) {
|
||||
val name: String get() = "$vorname $nachname"
|
||||
}
|
||||
|
||||
enum class LizenzKlasse(val label: String) {
|
||||
KEINE("-"),
|
||||
R1("R1"),
|
||||
R1D1("R1D1"),
|
||||
R1S1("R1S1"),
|
||||
R2("R2"),
|
||||
R2D2("R2D2"),
|
||||
R2S2("R2S2"),
|
||||
R3("R3"),
|
||||
R4("R4")
|
||||
}
|
||||
|
||||
enum class Sparte(val label: String) {
|
||||
KEINE("-"),
|
||||
DRESSUR("Dressur"),
|
||||
SPRINGEN("Springen"),
|
||||
VIELSEITIGKEIT("Vielseitigkeit"),
|
||||
VOLTIGIEREN("Voltigieren"),
|
||||
FAHREN("Fahren"),
|
||||
REINING("Reining")
|
||||
}
|
||||
|
||||
enum class ReiterStatus(val label: String, val color: Color) {
|
||||
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||
GESPERRT("Gesperrt", Color(0xFFC62828)),
|
||||
INAKTIV("Inaktiv", Color(0xFF757575))
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
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.runtime.Composable
|
||||
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.reiter.domain.LizenzKlasse
|
||||
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||
import at.mocode.frontend.features.reiter.domain.Sparte
|
||||
|
||||
@Composable
|
||||
fun ReiterScreen(
|
||||
viewModel: ReiterViewModel
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
|
||||
MsMasterDetailLayout(
|
||||
master = {
|
||||
ReiterListContent(
|
||||
uiState = uiState,
|
||||
onSearchChange = viewModel::onSearchQueryChange,
|
||||
onReiterSelected = viewModel::selectReiter
|
||||
)
|
||||
},
|
||||
detail = {
|
||||
if (uiState.isEditing) {
|
||||
ReiterEditorContent(
|
||||
uiState = uiState,
|
||||
onVornameChange = viewModel::onEditVornameChange,
|
||||
onNachnameChange = viewModel::onEditNameChange,
|
||||
onLizenzChange = viewModel::onEditLizenzChange,
|
||||
onSparteChange = viewModel::onEditSparteChange,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Reiter ausgewählt",
|
||||
subtitle = "Wählen Sie einen Reiter aus der Liste aus oder legen Sie einen neuen an."
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReiterListContent(
|
||||
uiState: ReiterUiState,
|
||||
onSearchChange: (String) -> Unit,
|
||||
onReiterSelected: (Reiter) -> 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 = "Vorname",
|
||||
weight = 1f,
|
||||
cellRenderer = { Text(it.vorname, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Nachname",
|
||||
weight = 1f,
|
||||
cellRenderer = { Text(it.nachname, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Lizenz",
|
||||
width = 80.dp,
|
||||
cellRenderer = { Text(it.lizenz.label, 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 = onReiterSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReiterEditorContent(
|
||||
uiState: ReiterUiState,
|
||||
onVornameChange: (String) -> Unit,
|
||||
onNachnameChange: (String) -> Unit,
|
||||
onLizenzChange: (LizenzKlasse) -> Unit,
|
||||
onSparteChange: (Sparte) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
MsActionToolbar(
|
||||
title = "Reiter Details",
|
||||
onSave = onSave,
|
||||
onCancel = onCancel
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editVorname,
|
||||
onValueChange = onVornameChange,
|
||||
label = "Vorname",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editName,
|
||||
onValueChange = onNachnameChange,
|
||||
label = "Nachname",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsEnumDropdown(
|
||||
label = "Lizenzklasse",
|
||||
options = LizenzKlasse.values(),
|
||||
selectedOption = uiState.editLizenz,
|
||||
onOptionSelected = onLizenzChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsEnumDropdown(
|
||||
label = "Hauptsparte",
|
||||
options = Sparte.values(),
|
||||
selectedOption = uiState.editSparte,
|
||||
onOptionSelected = onSparteChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Beispiel für ValidationWrapper
|
||||
MsValidationWrapper(
|
||||
messages = listOf(
|
||||
ValidationMessage("Warnung: Lizenz läuft in 14 Tagen ab.", ValidationSeverity.WARNING)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
"Zusätzliche Reiter-Informationen",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package at.mocode.frontend.features.reiter.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import at.mocode.frontend.features.reiter.domain.LizenzKlasse
|
||||
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||
import at.mocode.frontend.features.reiter.domain.ReiterStatus
|
||||
import at.mocode.frontend.features.reiter.domain.Sparte
|
||||
|
||||
/**
|
||||
* UI-State für die Reiter-Verwaltung.
|
||||
*/
|
||||
data class ReiterUiState(
|
||||
val searchResults: List<Reiter> = emptyList(),
|
||||
val searchQuery: String = "",
|
||||
val selectedReiter: Reiter? = null,
|
||||
val isEditing: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val editName: String = "",
|
||||
val editVorname: String = "",
|
||||
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||
val editSparte: Sparte = Sparte.KEINE,
|
||||
val editStatus: ReiterStatus = ReiterStatus.AKTIV
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel für die Reiter-Verwaltung.
|
||||
* In einem echten Szenario würden wir hier ein Repository injizieren.
|
||||
*/
|
||||
class ReiterViewModel {
|
||||
var uiState by mutableStateOf(ReiterUiState())
|
||||
private set
|
||||
|
||||
init {
|
||||
// Initialer Load (Mock-Daten)
|
||||
loadReiter()
|
||||
}
|
||||
|
||||
private fun loadReiter() {
|
||||
val mockData = listOf(
|
||||
Reiter("1", "Stefan", "Möbius", "123456", LizenzKlasse.R2D2, Sparte.DRESSUR, ReiterStatus.AKTIV),
|
||||
Reiter("2", "Julia", "Reiterin", "654321", LizenzKlasse.R1, Sparte.SPRINGEN, ReiterStatus.AKTIV),
|
||||
Reiter("3", "Max", "Mustermann", "112233", LizenzKlasse.KEINE, Sparte.KEINE, ReiterStatus.GESPERRT),
|
||||
Reiter("4", "Lisa", "Springen", "445566", LizenzKlasse.R3, Sparte.SPRINGEN, ReiterStatus.AKTIV)
|
||||
)
|
||||
uiState = uiState.copy(searchResults = mockData)
|
||||
}
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
uiState = uiState.copy(searchQuery = query)
|
||||
// Hier würde die Filter-Logik greifen
|
||||
}
|
||||
|
||||
fun selectReiter(reiter: Reiter) {
|
||||
uiState = uiState.copy(
|
||||
selectedReiter = reiter,
|
||||
isEditing = true,
|
||||
editVorname = reiter.vorname,
|
||||
editName = reiter.nachname,
|
||||
editLizenz = reiter.lizenz,
|
||||
editSparte = reiter.sparte,
|
||||
editStatus = reiter.status
|
||||
)
|
||||
}
|
||||
|
||||
fun onEditVornameChange(value: String) {
|
||||
uiState = uiState.copy(editVorname = value)
|
||||
}
|
||||
|
||||
fun onEditNameChange(value: String) {
|
||||
uiState = uiState.copy(editName = value)
|
||||
}
|
||||
|
||||
fun onEditLizenzChange(value: LizenzKlasse) {
|
||||
uiState = uiState.copy(editLizenz = value)
|
||||
}
|
||||
|
||||
fun onEditSparteChange(value: Sparte) {
|
||||
uiState = uiState.copy(editSparte = value)
|
||||
}
|
||||
|
||||
fun onSave() {
|
||||
// Mock-Speichern
|
||||
uiState = uiState.copy(isEditing = false)
|
||||
}
|
||||
|
||||
fun onCancel() {
|
||||
uiState = uiState.copy(isEditing = false)
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +126,7 @@ include(":frontend:features:zns-import-feature")
|
|||
include(":frontend:features:veranstalter-feature")
|
||||
include(":frontend:features:veranstaltung-feature")
|
||||
include(":frontend:features:profile-feature")
|
||||
include(":frontend:features:reiter-feature")
|
||||
include(":frontend:features:turnier-feature")
|
||||
include(":frontend:features:billing-feature")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user