diff --git a/docs/01_Architecture/Frontend_Komponenten_Roadmap.md b/docs/01_Architecture/Frontend_Komponenten_Roadmap.md index 468725b3..338abc98 100644 --- a/docs/01_Architecture/Frontend_Komponenten_Roadmap.md +++ b/docs/01_Architecture/Frontend_Komponenten_Roadmap.md @@ -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. diff --git a/frontend/features/reiter-feature/build.gradle.kts b/frontend/features/reiter-feature/build.gradle.kts new file mode 100644 index 00000000..b0411f12 --- /dev/null +++ b/frontend/features/reiter-feature/build.gradle.kts @@ -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) + } + } +} diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt new file mode 100644 index 00000000..2c748f7b --- /dev/null +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt @@ -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)) +} diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt new file mode 100644 index 00000000..61823413 --- /dev/null +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt @@ -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 + ) + } + } +} diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt new file mode 100644 index 00000000..e966fa1a --- /dev/null +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt @@ -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 = 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) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 191a106e..87aff866 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")