diff --git a/docs/01_Architecture/Frontend_Komponenten_Roadmap.md b/docs/01_Architecture/Frontend_Komponenten_Roadmap.md index 338abc98..6f8e12ab 100644 --- a/docs/01_Architecture/Frontend_Komponenten_Roadmap.md +++ b/docs/01_Architecture/Frontend_Komponenten_Roadmap.md @@ -62,7 +62,7 @@ Hier bringen wir alles zusammen, bevor das finale Routing implementiert wird. 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. +* [x] **Pferde-Verwaltung (MVP):** Analog zur Reiter-Verwaltung (Fertiggestellt). * [ ] **Navigation & Routing:** Integration der neuen Screens in die Hauptnavigation. --- diff --git a/docs/ScreenShots/preview-idea-vorschau_2026-03-31_11-48.png b/docs/ScreenShots/preview-idea-vorschau_2026-03-31_11-48.png new file mode 100644 index 00000000..cdb8649c Binary files /dev/null and b/docs/ScreenShots/preview-idea-vorschau_2026-03-31_11-48.png differ diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsCard.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsCard.kt index 0ae37b72..80be1f74 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsCard.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsCard.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults 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 @@ -40,3 +41,17 @@ fun MsCard( } } } + +// Preview für IDE (muss in jvmMain liegen um in IDEA gerendert zu werden, +// oder hier bleiben als Dokumentation) +@Composable +fun MsCardPreviewContent() { + MaterialTheme { + Column(modifier = Modifier.padding(16.dp)) { + MsCard { + Text("Dies ist eine MsCard", style = MaterialTheme.typography.bodyMedium) + Text("Mit High-Density Content.", style = MaterialTheme.typography.bodySmall) + } + } + } +} diff --git a/frontend/features/pferde-feature/build.gradle.kts b/frontend/features/pferde-feature/build.gradle.kts new file mode 100644 index 00000000..62eb61fe --- /dev/null +++ b/frontend/features/pferde-feature/build.gradle.kts @@ -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) + } + } +} diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt new file mode 100644 index 00000000..223f9a7e --- /dev/null +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt @@ -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)) +} diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt new file mode 100644 index 00000000..33cd2c15 --- /dev/null +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt @@ -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) + } + } +} diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt new file mode 100644 index 00000000..8c20154c --- /dev/null +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt @@ -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 = 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) + } +} 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 index 61823413..3d2d537b 100644 --- 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 @@ -136,7 +136,7 @@ private fun ReiterEditorContent( Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { MsEnumDropdown( label = "Lizenzklasse", - options = LizenzKlasse.values(), + options = LizenzKlasse.entries.toTypedArray(), selectedOption = uiState.editLizenz, onOptionSelected = onLizenzChange, optionLabel = { it.label }, @@ -144,7 +144,7 @@ private fun ReiterEditorContent( ) MsEnumDropdown( label = "Hauptsparte", - options = Sparte.values(), + options = Sparte.entries.toTypedArray(), selectedOption = uiState.editSparte, onOptionSelected = onSparteChange, optionLabel = { it.label }, @@ -167,3 +167,14 @@ private fun ReiterEditorContent( } } } + +@Composable +fun ReiterScreenPreviewContent() { + val viewModel = ReiterViewModel().apply { + // Optional: Hier könnten Mock-Daten direkt gesetzt werden, + // falls das ViewModel dies unterstützt. + } + MaterialTheme { + ReiterScreen(viewModel = viewModel) + } +} 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 index e966fa1a..2f22cc1b 100644 --- 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 @@ -28,13 +28,15 @@ data class ReiterUiState( * ViewModel für die Reiter-Verwaltung. * In einem echten Szenario würden wir hier ein Repository injizieren. */ -class ReiterViewModel { +open class ReiterViewModel(initialLoad: Boolean = true) { var uiState by mutableStateOf(ReiterUiState()) - private set + protected set init { - // Initialer Load (Mock-Daten) - loadReiter() + if (initialLoad) { + // Initialer Load (Mock-Daten) + loadReiter() + } } private fun loadReiter() { diff --git a/frontend/features/reiter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreenPreview.kt b/frontend/features/reiter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreenPreview.kt new file mode 100644 index 00000000..60c3fd48 --- /dev/null +++ b/frontend/features/reiter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreenPreview.kt @@ -0,0 +1,66 @@ +package at.mocode.frontend.features.reiter.presentation + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +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 +import at.mocode.wui.preview.ComponentPreview + +/** + * Hilf's-ViewModel für die Vorschau, um den Status direkt setzen zu können. + */ +private class PreviewReiterViewModel(initialState: ReiterUiState) : ReiterViewModel(initialLoad = false) { + init { + uiState = initialState + } +} + +@ComponentPreview +@Composable +fun PreviewReiterScreen_List() { + val viewModel = ReiterViewModel() // Nutzt die Mock-Daten aus dem init-Block + MaterialTheme { + ReiterScreen(viewModel = viewModel) + } +} + +@ComponentPreview +@Composable +fun PreviewReiterScreen_Editing() { + val mockReiter = Reiter( + id = "1", + vorname = "Stefan", + nachname = "Möbius", + satznummer = "123456", + lizenz = LizenzKlasse.R2D2, + sparte = Sparte.DRESSUR, + status = ReiterStatus.AKTIV + ) + val viewModel = PreviewReiterViewModel( + ReiterUiState( + searchResults = listOf(mockReiter), + selectedReiter = mockReiter, + isEditing = true, + editVorname = mockReiter.vorname, + editName = mockReiter.nachname, + editLizenz = mockReiter.lizenz, + editSparte = mockReiter.sparte, + editStatus = mockReiter.status + ) + ) + + MaterialTheme { + ReiterScreen(viewModel = viewModel) + } +} + +@ComponentPreview +@Composable +fun PreviewReiterScreen_Empty() { + val viewModel = PreviewReiterViewModel(ReiterUiState()) + MaterialTheme { + ReiterScreen(viewModel = viewModel) + } +} diff --git a/frontend/shells/meldestelle-desktop/build.gradle.kts b/frontend/shells/meldestelle-desktop/build.gradle.kts index f9c00a1a..1f2f2a0e 100644 --- a/frontend/shells/meldestelle-desktop/build.gradle.kts +++ b/frontend/shells/meldestelle-desktop/build.gradle.kts @@ -35,6 +35,8 @@ kotlin { implementation(projects.frontend.features.veranstaltungFeature) implementation(projects.frontend.features.turnierFeature) implementation(project(":frontend:features:profile-feature")) + implementation(project(":frontend:features:reiter-feature")) + implementation(project(":frontend:features:pferde-feature")) implementation(project(":frontend:features:billing-feature")) // Compose Desktop diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/PreviewMain.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/PreviewMain.kt index 5a0a89da..dae94d13 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/PreviewMain.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/PreviewMain.kt @@ -5,12 +5,8 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.window.singleWindowApplication -import at.mocode.turnier.feature.presentation.TurnierDetailScreen -import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen -import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen -import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen -import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen -import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen +import at.mocode.frontend.features.pferde.presentation.PferdeScreen +import at.mocode.frontend.features.pferde.presentation.PferdeViewModel /** * Hot-Reload Preview Entry Point @@ -31,6 +27,13 @@ fun main() = singleWindowApplication(title = "🔥 Hot-Reload Preview") { private fun PreviewContent() { MaterialTheme { Surface { + + // --- REITER --- + // ReiterScreen(viewModel = ReiterViewModel()) + + // --- PFERDE --- + PferdeScreen(viewModel = PferdeViewModel()) + // ── Hier den gewünschten Screen eintragen ────────────────────── // VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {}) // VeranstalterNeuScreen(onBack = {}, onSave = {}) @@ -40,11 +43,11 @@ private fun PreviewContent() { // ────────────────────────────────────────────────────────────── // Standard: AdminUebersichtScreen (Startseite nach Login) - AdminUebersichtScreen( - onVeranstalterAuswahl = {}, - onVeranstaltungOeffnen = {}, - onPingService = {} - ) +// AdminUebersichtScreen( +// onVeranstalterAuswahl = {}, +// onVeranstaltungOeffnen = {}, +// onPingService = {} +// ) } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 87aff866..5345d7d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,6 +127,7 @@ include(":frontend:features:veranstalter-feature") include(":frontend:features:veranstaltung-feature") include(":frontend:features:profile-feature") include(":frontend:features:reiter-feature") +include(":frontend:features:pferde-feature") include(":frontend:features:turnier-feature") include(":frontend:features:billing-feature")