feat(verein-feature): add Vereinsverwaltung module with screens, ViewModel, and integration
- Introduced `verein-feature` module for managing Vereine, including list, detail, and editor views using `MsMasterDetailLayout`. - Added new domain models (`Verein`, `VereinStatus`) and integrated mock data for development. - Registered the new feature in `settings.gradle.kts` and `DesktopMainLayout.kt`, including breadcrumb navigation and entry point. - Updated `VeranstaltungenUebersichtV2` to add Vereine as a quick-access KPI tile. - Removed unnecessary logout functionality and adjusted the root navigation for consistency. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+8
-4
@@ -1,6 +1,7 @@
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -9,8 +10,8 @@ import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -39,6 +40,7 @@ fun AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onPingService: () -> Unit = {},
|
||||
onVereineOeffnen: () -> Unit = {},
|
||||
) {
|
||||
// Placeholder-Daten für die UI-Struktur (sichtbar als Cards)
|
||||
val sample = listOf(
|
||||
@@ -66,6 +68,7 @@ fun AdminUebersichtScreen(
|
||||
inVorbereitung = 0,
|
||||
gesamt = 0,
|
||||
archiv = 0,
|
||||
onVereineClick = onVereineOeffnen
|
||||
)
|
||||
|
||||
// Toolbar
|
||||
@@ -155,6 +158,7 @@ private fun KpiKachelRow(
|
||||
inVorbereitung: Int,
|
||||
gesamt: Int,
|
||||
archiv: Int,
|
||||
onVereineClick: () -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -175,10 +179,10 @@ private fun KpiKachelRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
KpiKachel(
|
||||
label = "GESAMT",
|
||||
wert = gesamt.toString(),
|
||||
label = "VEREINE",
|
||||
wert = "4", // Mock
|
||||
akzentFarbe = Color(0xFF6B7280),
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier.weight(1f).clickable { onVereineClick() },
|
||||
)
|
||||
KpiKachel(
|
||||
label = "ARCHIV",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Feature-Modul: Vereins-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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package at.mocode.frontend.features.verein.domain
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* UI-Modell für einen Verein.
|
||||
*/
|
||||
data class Verein(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val langname: String? = null,
|
||||
val oepsNr: String? = null,
|
||||
val ort: String? = null,
|
||||
val plz: String? = null,
|
||||
val land: String = "AUT",
|
||||
val status: VereinStatus = VereinStatus.AKTIV
|
||||
)
|
||||
|
||||
enum class VereinStatus(val label: String, val color: Color) {
|
||||
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||
RUHEND("Ruhend", Color(0xFFE65100)),
|
||||
AUFGELOEST("Aufgelöst", Color(0xFFC62828))
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package at.mocode.frontend.features.verein.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.verein.domain.Verein
|
||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||
|
||||
@Composable
|
||||
fun VereinScreen(
|
||||
viewModel: VereinViewModel
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
|
||||
MsMasterDetailLayout(
|
||||
master = {
|
||||
VereinListContent(
|
||||
uiState = uiState,
|
||||
onSearchChange = viewModel::onSearchQueryChange,
|
||||
onVereinSelected = viewModel::selectVerein,
|
||||
onAddNew = viewModel::onAddNew
|
||||
)
|
||||
},
|
||||
detail = {
|
||||
if (uiState.isEditing) {
|
||||
VereinEditorContent(
|
||||
uiState = uiState,
|
||||
onNameChange = viewModel::onEditNameChange,
|
||||
onLangnameChange = viewModel::onEditLangnameChange,
|
||||
onOepsNrChange = viewModel::onEditOepsNrChange,
|
||||
onOrtChange = viewModel::onEditOrtChange,
|
||||
onPlzChange = viewModel::onEditPlzChange,
|
||||
onStatusChange = viewModel::onEditStatusChange,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Verein ausgewählt",
|
||||
subtitle = "Wählen Sie einen Verein aus der Liste aus oder legen Sie einen neuen an."
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VereinListContent(
|
||||
uiState: VereinUiState,
|
||||
onSearchChange: (String) -> Unit,
|
||||
onVereinSelected: (Verein) -> Unit,
|
||||
onAddNew: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
MsFilterBar(
|
||||
searchQuery = uiState.searchQuery,
|
||||
onSearchQueryChange = onSearchChange,
|
||||
resultCount = uiState.searchResults.size,
|
||||
actions = {
|
||||
MsButton(
|
||||
text = "Neu",
|
||||
onClick = onAddNew,
|
||||
variant = ButtonVariant.PRIMARY,
|
||||
size = ButtonSize.SMALL
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
MsDataTable(
|
||||
items = uiState.searchResults,
|
||||
columns = listOf(
|
||||
MsColumnDefinition(
|
||||
title = "Name",
|
||||
weight = 1.5f,
|
||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Ort",
|
||||
weight = 1f,
|
||||
cellRenderer = { Text(it.ort ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "OePS-Nr",
|
||||
width = 100.dp,
|
||||
cellRenderer = { Text(it.oepsNr ?: "-", 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 = onVereinSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VereinEditorContent(
|
||||
uiState: VereinUiState,
|
||||
onNameChange: (String) -> Unit,
|
||||
onLangnameChange: (String) -> Unit,
|
||||
onOepsNrChange: (String) -> Unit,
|
||||
onOrtChange: (String) -> Unit,
|
||||
onPlzChange: (String) -> Unit,
|
||||
onStatusChange: (VereinStatus) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
MsActionToolbar(
|
||||
title = if (uiState.selectedVerein == null) "Neuer Verein" else "Verein Details",
|
||||
onSave = onSave,
|
||||
onCancel = onCancel
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
MsTextField(
|
||||
value = uiState.editName,
|
||||
onValueChange = onNameChange,
|
||||
label = "Name (Kurz)",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
MsTextField(
|
||||
value = uiState.editLangname,
|
||||
onValueChange = onLangnameChange,
|
||||
label = "Vollständiger Name",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editOepsNr,
|
||||
onValueChange = onOepsNrChange,
|
||||
label = "OePS-Nr",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsEnumDropdown(
|
||||
label = "Status",
|
||||
options = VereinStatus.entries.toTypedArray(),
|
||||
selectedOption = uiState.editStatus,
|
||||
onOptionSelected = onStatusChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editPlz,
|
||||
onValueChange = onPlzChange,
|
||||
label = "PLZ",
|
||||
modifier = Modifier.weight(0.3f)
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editOrt,
|
||||
onValueChange = onOrtChange,
|
||||
label = "Ort",
|
||||
modifier = Modifier.weight(0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package at.mocode.frontend.features.verein.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.mocode.frontend.features.verein.domain.Verein
|
||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||
|
||||
/**
|
||||
* UI-State für die Vereins-Verwaltung.
|
||||
*/
|
||||
data class VereinUiState(
|
||||
val allVereine: List<Verein> = emptyList(),
|
||||
val searchResults: List<Verein> = emptyList(),
|
||||
val searchQuery: String = "",
|
||||
val selectedVerein: Verein? = null,
|
||||
val isEditing: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val editName: String = "",
|
||||
val editLangname: String = "",
|
||||
val editOepsNr: String = "",
|
||||
val editOrt: String = "",
|
||||
val editPlz: String = "",
|
||||
val editStatus: VereinStatus = VereinStatus.AKTIV
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel für die Vereins-Verwaltung.
|
||||
*/
|
||||
open class VereinViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||
var uiState by mutableStateOf(VereinUiState())
|
||||
protected set
|
||||
|
||||
init {
|
||||
if (initialLoad) {
|
||||
loadVereine()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVereine() {
|
||||
val mockData = listOf(
|
||||
Verein("1", "URV Neumarkt", "Union Reit- und Fahrverein Neumarkt", "4-201", "Neumarkt", "4212"),
|
||||
Verein("2", "RV Linz", "Reitverein Linz-Ebelsberg", "4-001", "Linz", "4030"),
|
||||
Verein("3", "RC Stadl-Paura", "Reitclub Pferdewelt Stadl-Paura", "4-100", "Stadl-Paura", "4650"),
|
||||
Verein("4", "Union Reitverein X", null, "1-123", "Wien", "1010", status = VereinStatus.RUHEND)
|
||||
)
|
||||
uiState = uiState.copy(
|
||||
allVereine = mockData,
|
||||
searchResults = mockData
|
||||
)
|
||||
}
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
uiState = uiState.copy(searchQuery = query)
|
||||
filterResults()
|
||||
}
|
||||
|
||||
private fun filterResults() {
|
||||
val query = uiState.searchQuery.lowercase()
|
||||
val filtered = if (query.isEmpty()) {
|
||||
uiState.allVereine
|
||||
} else {
|
||||
uiState.allVereine.filter {
|
||||
it.name.lowercase().contains(query) ||
|
||||
it.oepsNr?.lowercase()?.contains(query) == true ||
|
||||
it.ort?.lowercase()?.contains(query) == true
|
||||
}
|
||||
}
|
||||
uiState = uiState.copy(searchResults = filtered)
|
||||
}
|
||||
|
||||
fun selectVerein(verein: Verein) {
|
||||
uiState = uiState.copy(
|
||||
selectedVerein = verein,
|
||||
isEditing = true,
|
||||
editName = verein.name,
|
||||
editLangname = verein.langname ?: "",
|
||||
editOepsNr = verein.oepsNr ?: "",
|
||||
editOrt = verein.ort ?: "",
|
||||
editPlz = verein.plz ?: "",
|
||||
editStatus = verein.status
|
||||
)
|
||||
}
|
||||
|
||||
fun onEditNameChange(value: String) {
|
||||
uiState = uiState.copy(editName = value)
|
||||
}
|
||||
|
||||
fun onEditLangnameChange(value: String) {
|
||||
uiState = uiState.copy(editLangname = value)
|
||||
}
|
||||
|
||||
fun onEditOepsNrChange(value: String) {
|
||||
uiState = uiState.copy(editOepsNr = value)
|
||||
}
|
||||
|
||||
fun onEditOrtChange(value: String) {
|
||||
uiState = uiState.copy(editOrt = value)
|
||||
}
|
||||
|
||||
fun onEditPlzChange(value: String) {
|
||||
uiState = uiState.copy(editPlz = value)
|
||||
}
|
||||
|
||||
fun onEditStatusChange(value: VereinStatus) {
|
||||
uiState = uiState.copy(editStatus = value)
|
||||
}
|
||||
|
||||
fun onSave() {
|
||||
// Mock-Speichern
|
||||
uiState = uiState.copy(isEditing = false)
|
||||
}
|
||||
|
||||
fun onCancel() {
|
||||
uiState = uiState.copy(isEditing = false)
|
||||
}
|
||||
|
||||
fun onAddNew() {
|
||||
uiState = uiState.copy(
|
||||
selectedVerein = null,
|
||||
isEditing = true,
|
||||
editName = "",
|
||||
editLangname = "",
|
||||
editOepsNr = "",
|
||||
editOrt = "",
|
||||
editPlz = "",
|
||||
editStatus = VereinStatus.AKTIV
|
||||
)
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package at.mocode.frontend.features.verein.di
|
||||
|
||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val vereinFeatureModule = module {
|
||||
viewModelOf(::VereinViewModel)
|
||||
}
|
||||
Reference in New Issue
Block a user