chore: entferne settings.json und Veranstaltungskomponenten, refaktoriere Veranstaltungsverwaltung und implementiere StoreVeranstaltungRepository

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-20 18:33:38 +02:00
parent edfe05cbe3
commit db58c24613
17 changed files with 512 additions and 383 deletions

View File

@ -32,6 +32,16 @@ Die Nachmittags-Session am 20. April 2026 wurde erfolgreich abgeschlossen. Die g
- **API-Bereinigung:** Beseitigung ungenutzter Properties (z.B. `TopBarTextColor`) und Korrektur veralteter API-Signaturen in den Screen-Injektionen. - **API-Bereinigung:** Beseitigung ungenutzter Properties (z.B. `TopBarTextColor`) und Korrektur veralteter API-Signaturen in den Screen-Injektionen.
- **Fehlerbehebung:** Beseitigung von Kompilerfehlern in `NavRail.kt` (Tooltip-Positionierung) und Bereinigung ungenutzter Parameter in `ContentArea.kt`. - **Fehlerbehebung:** Beseitigung von Kompilerfehlern in `NavRail.kt` (Tooltip-Positionierung) und Bereinigung ungenutzter Parameter in `ContentArea.kt`.
### 4. Fachlicher Einstieg & Start-Screen (Punkt 4)
- **Extraktion:** Die Veranstaltungs-Verwaltung wurde aus der Shell in das Feature-Modul `veranstaltung-feature` extrahiert.
- **Architektur:** Implementierung von `VeranstaltungManagementViewModel` und `VeranstaltungRepository` (ADR-0024 konform).
- **Entkoppelung:** Einführung eines domänenspezifischen `VeranstaltungModel` zur Trennung von Shell-Datenstrukturen.
- **UI/UX (Vision_03):**
- High-Density Layout mit optimierten Cards und Spacings.
- Implementierung einer Echtzeit-Suche und Status-Filtern (Alle, In Planung, Aktiv, Abgeschlossen).
- Konsistente Status-Badges nach dem offiziellen Farbschema.
- **Cleanup:** Löschung der redundanten `VeranstaltungVerwaltung.kt` in der Desktop-Shell.
## 📐 Architektur-Check (ADR-0024) ## 📐 Architektur-Check (ADR-0024)
- **Self-Contained:** Feature-Module verwalten ihren State; Shell reagiert auf Events. - **Self-Contained:** Feature-Module verwalten ihren State; Shell reagiert auf Events.
- **Reaktivität:** UI reagiert sofort auf Konfigurationsänderungen (`settings.json`). - **Reaktivität:** UI reagiert sofort auf Konfigurationsänderungen (`settings.json`).
@ -39,3 +49,9 @@ Die Nachmittags-Session am 20. April 2026 wurde erfolgreich abgeschlossen. Die g
--- ---
*Dokumentation erstellt durch den Curator im Rahmen des "Meldestelle"-Protokolls.* *Dokumentation erstellt durch den Curator im Rahmen des "Meldestelle"-Protokolls.*
### 5. Hotfixes & Stabilisierung (Post-Release Review)
- **Navigations-Sicherheit:** Das Logo-Icon in der `NavRail` wurde gesperrt (`enabled = isConfigured`), um unautorisierte Zugriffe vor dem Onboarding zu unterbinden.
- **Koin-Fix:** Registrierung des `veranstalterModule` in der `main.kt` nachgeholt, um Abstürze beim Erstellen neuer Veranstaltungen zu beheben.
- **UI-Polishing:** Entfernung des irritierenden Zurück-Pfeils in der Konnektivitäts-Diagnose (`PingScreen`), um die UX-Klarheit zu erhöhen.
- **Home-Navigation-Sperre:** Das Home-Icon im Header wurde ebenfalls an den `isConfigured`-Status gebunden, um die Start-Sequenz final abzusichern.

View File

@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -23,35 +22,42 @@ import at.mocode.frontend.core.designsystem.theme.Dimens
@Composable @Composable
fun MsCard( fun MsCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
Card( if (onClick != null) {
modifier = modifier, Card(
shape = MaterialTheme.shapes.medium, onClick = onClick,
colors = CardDefaults.cardColors( modifier = modifier,
containerColor = MaterialTheme.colorScheme.surface shape = MaterialTheme.shapes.medium,
), colors = CardDefaults.cardColors(
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), // Dünner Rahmen containerColor = MaterialTheme.colorScheme.surface
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // Kein Schatten ),
) { border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), // Dünner Rahmen
Column( elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // Kein Schatten
modifier = Modifier.padding(Dimens.SpacingS) // Kompaktes Padding innen
) { ) {
content() Column(
modifier = Modifier.padding(Dimens.SpacingS) // Kompaktes Padding innen
) {
content()
}
} }
} } else {
} Card(
modifier = modifier,
// Preview für IDE (muss in jvmMain liegen um in IDEA gerendert zu werden, shape = MaterialTheme.shapes.medium,
// oder hier bleiben als Dokumentation) colors = CardDefaults.cardColors(
@Composable containerColor = MaterialTheme.colorScheme.surface
fun MsCardPreviewContent() { ),
MaterialTheme { border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), // Dünner Rahmen
Column(modifier = Modifier.padding(16.dp)) { elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // Kein Schatten
MsCard { ) {
Text("Dies ist eine MsCard", style = MaterialTheme.typography.bodyMedium) Column(
Text("Mit High-Density Content.", style = MaterialTheme.typography.bodySmall) modifier = Modifier.padding(Dimens.SpacingS) // Kompaktes Padding innen
) {
content()
} }
} }
} }
} }

View File

@ -0,0 +1,8 @@
package at.mocode.veranstaltung.feature.di
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
import org.koin.dsl.module
val veranstaltungModule = module {
factory { VeranstaltungManagementViewModel(get()) }
}

View File

@ -0,0 +1,16 @@
package at.mocode.veranstaltung.feature.domain.model
import at.mocode.core.domain.model.VeranstaltungsStatusE
import kotlinx.datetime.LocalDate
data class VeranstaltungModel(
val id: Long,
val veranstalterId: Long,
val titel: String,
val datumVon: LocalDate,
val datumBis: LocalDate?,
val status: VeranstaltungsStatusE,
val ort: String,
val vereinName: String,
val logoUrl: String? = null
)

View File

@ -0,0 +1,10 @@
package at.mocode.veranstaltung.feature.domain.repository
import at.mocode.veranstaltung.feature.domain.model.VeranstaltungModel
import kotlinx.coroutines.flow.Flow
interface VeranstaltungRepository {
fun getAllVeranstaltungen(): Flow<List<VeranstaltungModel>>
fun getVeranstaltungenByStatus(status: String): Flow<List<VeranstaltungModel>>
suspend fun deleteVeranstaltung(veranstalterId: Long, veranstaltungId: Long)
}

View File

@ -3,65 +3,150 @@ package at.mocode.veranstaltung.feature.presentation
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Place
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.models.PlaceholderContent import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.veranstaltung.feature.domain.model.VeranstaltungModel
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
import kotlinx.coroutines.flow.firstOrNull
/**
* Detailansicht einer bestehenden Veranstaltung (Vision_03: /veranstaltung/{id}).
* Zeigt Übersicht-Tab mit Turniere-Section.
* TODO: Echte Daten laden (Phase 4/5).
*/
@Composable @Composable
fun VeranstaltungDetailScreen( fun VeranstaltungDetailScreen(
veranstaltungId: Long, veranstaltungId: Long,
onBack: () -> Unit, repository: VeranstaltungRepository,
onTurnierNeu: () -> Unit, onBack: () -> Unit,
onTurnierOeffnen: (Long) -> Unit, onTurnierNeu: () -> Unit,
onTurnierOpen: (Long) -> Unit,
onNavigateToVeranstalterProfil: (Long) -> Unit
) { ) {
Column(modifier = Modifier.fillMaxSize()) { var veranstaltung by remember { mutableStateOf<VeranstaltungModel?>(null) }
// Toolbar var isLoading by remember { mutableStateOf(true) }
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp), LaunchedEffect(veranstaltungId) {
verticalAlignment = Alignment.CenterVertically, isLoading = true
) { // Einfache Abfrage über das Repository (In Phase 14 wird das ViewModel mit StateFlow)
IconButton(onClick = onBack) { val all = repository.getAllVeranstaltungen().firstOrNull()
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") veranstaltung = all?.find { it.id == veranstaltungId }
} isLoading = false
Spacer(Modifier.width(8.dp))
Text(
text = "Veranstaltung #$veranstaltungId",
style = MaterialTheme.typography.headlineSmall,
)
} }
PrimaryTabRow(selectedTabIndex = 0) { if (isLoading) {
Tab(selected = true, onClick = {}, text = { Text("Veranstaltung Übersicht") }) Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
} CircularProgressIndicator()
}
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) { return
// Turniere-Section }
Row(
modifier = Modifier.fillMaxWidth(), val event = veranstaltung
horizontalArrangement = Arrangement.SpaceBetween, if (event == null) {
verticalAlignment = Alignment.CenterVertically, Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
) { Text("Veranstaltung #$veranstaltungId nicht gefunden.")
Text("Turniere", style = MaterialTheme.typography.titleMedium) }
OutlinedButton(onClick = onTurnierNeu) { return
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp)) }
Spacer(Modifier.width(4.dp))
Text("Neues Turnier") Column(Modifier.fillMaxSize().padding(Dimens.SpacingM)) {
// Header
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
) {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Column {
Text(
text = event.titel,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "${event.datumVon} - ${event.datumBis ?: ""}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(Modifier.weight(1f))
Button(onClick = onTurnierNeu) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(Dimens.SpacingXS))
Text("Turnier hinzufügen")
}
}
Spacer(Modifier.height(Dimens.SpacingL))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
// Linke Spalte: Details & Turniere
Column(Modifier.weight(2f), verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
// KPIs
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
DetailKpiCard("Status", event.status.name, Icons.Default.Info, Modifier.weight(1f))
DetailKpiCard("Ort", event.ort, Icons.Default.Place, Modifier.weight(1f))
}
Text(
text = "Turniere in dieser Veranstaltung",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Card(Modifier.fillMaxWidth()) {
Box(Modifier.padding(Dimens.SpacingXL).fillMaxWidth(), contentAlignment = Alignment.Center) {
Text("Noch keine Turniere angelegt (Phase 14).", style = MaterialTheme.typography.bodyMedium)
}
}
}
// Rechte Spalte: Veranstalter Information & Aktionen
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
Card {
Column(Modifier.padding(Dimens.SpacingM), verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
Text("Veranstalter", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(event.vereinName, style = MaterialTheme.typography.bodyLarge)
Text("ID: ${event.veranstalterId}", style = MaterialTheme.typography.bodySmall)
TextButton(onClick = { onNavigateToVeranstalterProfil(event.veranstalterId) }) {
Text("Vereinsprofil öffnen")
Icon(Icons.AutoMirrored.Filled.OpenInNew, contentDescription = null, modifier = Modifier.size(16.dp))
}
}
}
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f))) {
Column(Modifier.padding(Dimens.SpacingM), verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
Text("Schnell-Aktionen", style = MaterialTheme.typography.labelLarge)
Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Ausschreibung (ZNS)") }
OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Programmheft drucken") }
}
}
}
}
}
}
@Composable
private fun DetailKpiCard(label: String, wert: String, icon: ImageVector, modifier: Modifier = Modifier) {
Card(modifier = modifier) {
Row(
modifier = Modifier.padding(Dimens.SpacingS),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
) {
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp))
Column {
Text(label, style = MaterialTheme.typography.labelSmall)
Text(wert, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
}
} }
}
Spacer(Modifier.height(16.dp))
PlaceholderContent(
title = "Noch keine Turniere",
subtitle = "Lege ein neues Turnier für diese Veranstaltung an.",
)
} }
}
} }

View File

@ -0,0 +1,69 @@
package at.mocode.veranstaltung.feature.presentation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.core.domain.model.VeranstaltungsStatusE
import at.mocode.veranstaltung.feature.domain.model.VeranstaltungModel
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
data class VeranstaltungManagementState(
val veranstaltungen: List<VeranstaltungModel> = emptyList(),
val searchQuery: String = "",
val selectedStatus: VeranstaltungsStatusE? = null,
val isLoading: Boolean = false,
val error: String? = null
)
class VeranstaltungManagementViewModel(
private val repository: VeranstaltungRepository
) : ViewModel() {
var state by mutableStateOf(VeranstaltungManagementState())
private set
init {
loadVeranstaltungen()
}
private fun loadVeranstaltungen() {
viewModelScope.launch {
state = state.copy(isLoading = true)
repository.getAllVeranstaltungen().collectLatest { list ->
state = state.copy(
veranstaltungen = list,
isLoading = false
)
}
}
}
fun onSearchQueryChanged(query: String) {
state = state.copy(searchQuery = query)
}
fun onStatusFilterChanged(status: VeranstaltungsStatusE?) {
state = state.copy(selectedStatus = status)
}
val filteredVeranstaltungen: List<VeranstaltungModel>
get() {
return state.veranstaltungen.filter { event ->
val matchesSearch = event.titel.contains(state.searchQuery, ignoreCase = true) ||
event.vereinName.contains(state.searchQuery, ignoreCase = true)
val matchesStatus = state.selectedStatus == null || event.status == state.selectedStatus
matchesSearch && matchesStatus
}.sortedByDescending { it.datumVon }
}
fun deleteVeranstaltung(event: VeranstaltungModel) {
viewModelScope.launch {
repository.deleteVeranstaltung(event.veranstalterId, event.id)
loadVeranstaltungen() // Refresh
}
}
}

View File

@ -1,69 +1,39 @@
package at.mocode.veranstaltung.feature.presentation package at.mocode.veranstaltung.feature.presentation
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Event import androidx.compose.material.icons.filled.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import at.mocode.core.domain.model.VeranstaltungsStatusE
import at.mocode.frontend.core.designsystem.components.MsButton import at.mocode.frontend.core.designsystem.components.MsButton
import at.mocode.frontend.core.designsystem.components.MsCard import at.mocode.frontend.core.designsystem.components.MsCard
import at.mocode.frontend.core.designsystem.theme.Dimens import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.veranstaltung.feature.domain.model.VeranstaltungModel
import org.koin.compose.koinInject
import java.util.Locale.getDefault
/** /**
* UI-Modell für die Anzeige einer Veranstaltung in der Liste. * Veranstaltungs-Übersicht (Einstieg nach Onboarding gemäß ADR-0024).
*/ * Zeigt eine Liste aller Veranstaltungen mit Such- und Filterfunktion.
data class VeranstaltungSimpleUiModel(
val id: Long,
val name: String,
val untertitel: String?,
val ort: String,
val datum: String,
val logoUrl: String? = null
)
/**
* Veranstaltungs-Übersicht (Drawer-Einstieg gemäß Vision_03).
* Zeigt Liste aller Veranstaltungen + Button "Neue Veranstaltung".
*/ */
@Composable @Composable
fun VeranstaltungenScreen( fun VeranstaltungenScreen(
viewModel: VeranstaltungManagementViewModel = koinInject(),
onVeranstaltungNeu: () -> Unit, onVeranstaltungNeu: () -> Unit,
onVeranstaltungOeffnen: (Long) -> Unit, onVeranstaltungOeffnen: (Long, Long) -> Unit,
) { ) {
// Später: Echte Daten aus dem ViewModel laden val state = viewModel.state
val veranstaltungen = remember { val filteredVeranstaltungen = viewModel.filteredVeranstaltungen
mutableStateListOf(
VeranstaltungSimpleUiModel(
id = 1L,
name = "Springturnier Neumarkt",
untertitel = "CSN-B* | 24. - 26. April 2026",
ort = "Neumarkt am Wallersee",
datum = "24.04.2026 - 26.04.2026"
),
VeranstaltungSimpleUiModel(
id = 2L,
name = "Dressurtage Lamprechtshausen",
untertitel = "CDN-A* | 01. - 03. Mai 2026",
ort = "Lamprechtshausen",
datum = "01.05.2026 - 03.05.2026"
)
)
}
Column(modifier = Modifier.fillMaxSize().padding(Dimens.SpacingL)) { Column(modifier = Modifier.fillMaxSize().padding(Dimens.SpacingL)) {
// Header: Titel + Action // Header: Titel + Action
@ -80,29 +50,91 @@ fun VeranstaltungenScreen(
MsButton( MsButton(
text = "Neue Veranstaltung", text = "Neue Veranstaltung",
onClick = onVeranstaltungNeu onClick = onVeranstaltungNeu
// icon = Icons.Default.Add // MsButton unterstützt noch kein Icon im Parameter
) )
} }
Spacer(Modifier.height(Dimens.SpacingL)) Spacer(Modifier.height(Dimens.SpacingL))
if (veranstaltungen.isEmpty()) { // Suche & Filter (Vision_03 High-Density)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Row(
Text( verticalAlignment = Alignment.CenterVertically,
"Keine Veranstaltungen gefunden. Legen Sie eine neue an.", horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
style = MaterialTheme.typography.bodyLarge, modifier = Modifier.fillMaxWidth()
color = MaterialTheme.colorScheme.onSurfaceVariant ) {
OutlinedTextField(
value = state.searchQuery,
onValueChange = { viewModel.onSearchQueryChanged(it) },
placeholder = { Text("Suche nach Titel oder Verein...", style = MaterialTheme.typography.bodyMedium) },
modifier = Modifier.weight(1f),
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(20.dp)) },
trailingIcon = {
if (state.searchQuery.isNotEmpty()) {
IconButton(onClick = { viewModel.onSearchQueryChanged("") }) {
Icon(Icons.Default.Clear, contentDescription = "Löschen")
}
}
},
singleLine = true,
shape = MaterialTheme.shapes.medium,
textStyle = MaterialTheme.typography.bodyMedium
)
// Status Filter Chips
Row(
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS),
verticalAlignment = Alignment.CenterVertically
) {
FilterChip(
selected = state.selectedStatus == null,
onClick = { viewModel.onStatusFilterChanged(null) },
label = { Text("Alle", style = MaterialTheme.typography.labelMedium) }
) )
VeranstaltungsStatusE.entries.filter { it == VeranstaltungsStatusE.IN_PLANUNG || it == VeranstaltungsStatusE.ABGESCHLOSSEN || it == VeranstaltungsStatusE.AKTIV }
.forEach { status ->
FilterChip(
selected = state.selectedStatus == status,
onClick = { viewModel.onStatusFilterChanged(status) },
label = {
Text(
status.name.lowercase()
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
style = MaterialTheme.typography.labelMedium
)
}
)
}
}
}
Spacer(Modifier.height(Dimens.SpacingL))
if (filteredVeranstaltungen.isEmpty() && !state.isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Default.EventBusy,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = Color.LightGray
)
Spacer(Modifier.height(Dimens.SpacingM))
Text(
"Keine Veranstaltungen gefunden.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM) verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
contentPadding = PaddingValues(bottom = Dimens.SpacingL)
) { ) {
items(veranstaltungen) { event -> items(filteredVeranstaltungen) { event ->
VeranstaltungCard( VeranstaltungCard(
event = event, event = event,
onDoubleClick = { onVeranstaltungOeffnen(event.id) } onClick = { onVeranstaltungOeffnen(event.veranstalterId, event.id) }
) )
} }
} }
@ -110,37 +142,32 @@ fun VeranstaltungenScreen(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun VeranstaltungCard( fun VeranstaltungCard(
event: VeranstaltungSimpleUiModel, event: VeranstaltungModel,
onDoubleClick: () -> Unit onClick: () -> Unit
) { ) {
MsCard( MsCard(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() onClick = onClick
.combinedClickable(
onClick = { /* Einfacher Klick für Selektion, falls gewünscht */ },
onDoubleClick = onDoubleClick
)
) { ) {
Row( Row(
modifier = Modifier.padding(Dimens.SpacingS), modifier = Modifier.padding(Dimens.SpacingM),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Platzhalter für Logo // Logo / Icon
Box( Box(
modifier = Modifier modifier = Modifier
.size(64.dp) .size(48.dp)
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceVariant), .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image( Icon(
imageVector = Icons.Default.Event, imageVector = Icons.Default.CalendarToday,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(32.dp), modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant) tint = MaterialTheme.colorScheme.primary
) )
} }
@ -148,24 +175,54 @@ fun VeranstaltungCard(
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = event.name, text = event.titel,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
if (!event.untertitel.isNullOrBlank()) { Text(
text = event.vereinName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
) {
Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.size(14.dp), tint = Color.Gray)
Text(event.ort, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
Text("", color = Color.Gray)
Text( Text(
text = event.untertitel, "${event.datumVon} - ${event.datumBis ?: ""}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = Color.Gray
) )
} }
Spacer(Modifier.height(4.dp)) }
// Status Badge
Surface(
color = when (event.status) {
VeranstaltungsStatusE.ABGESCHLOSSEN -> Color(0xFFE8F5E9)
VeranstaltungsStatusE.IN_PLANUNG -> Color(0xFFE3F2FD)
else -> MaterialTheme.colorScheme.secondaryContainer
},
shape = MaterialTheme.shapes.small
) {
Text( Text(
text = "${event.ort} | ${event.datum}", text = event.status.name.lowercase()
style = MaterialTheme.typography.bodyMedium, .replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
color = MaterialTheme.colorScheme.onSurface modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = when (event.status) {
VeranstaltungsStatusE.ABGESCHLOSSEN -> Color(0xFF2E7D32)
VeranstaltungsStatusE.IN_PLANUNG -> Color(0xFF1976D2)
else -> MaterialTheme.colorScheme.onSecondaryContainer
}
) )
} }
Spacer(Modifier.width(Dimens.SpacingM))
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = Color.LightGray)
} }
} }
} }

View File

@ -45,6 +45,7 @@ kotlin {
jvmMain.dependencies { jvmMain.dependencies {
// Core-Module // Core-Module
implementation(projects.frontend.core.domain) implementation(projects.frontend.core.domain)
implementation(projects.core.coreDomain)
implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.navigation) implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.network) implementation(projects.frontend.core.network)

View File

@ -1,12 +0,0 @@
{
"deviceName": "Meldestelle",
"sharedKey": "Password",
"backupPath": "/mocode/meldestelle/docs/temp",
"networkRole": "MASTER",
"expectedClients": [
{
"name": "Richter-Turm",
"role": "RICHTER"
}
]
}

View File

@ -0,0 +1,59 @@
package at.mocode.frontend.shell.desktop.data.repository
import at.mocode.core.domain.model.VeranstaltungsStatusE
import at.mocode.frontend.shell.desktop.data.Store
import at.mocode.veranstaltung.feature.domain.model.VeranstaltungModel
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.datetime.LocalDate
/**
* Brücken-Implementierung, die den bestehenden Store nutzt.
* Liegt in der Shell, da sie Zugriff auf den Shell-spezifischen [Store] benötigt.
*/
class StoreVeranstaltungRepository : VeranstaltungRepository {
override fun getAllVeranstaltungen(): Flow<List<VeranstaltungModel>> = flow {
val allEvents = Store.allEvents().map { event ->
val verein = Store.vereine.find { it.id == event.veranstalterId }
VeranstaltungModel(
id = event.id,
veranstalterId = event.veranstalterId,
titel = event.titel,
datumVon = try {
LocalDate.parse(event.datumVon)
} catch (_: Exception) {
LocalDate(2026, 4, 20)
},
datumBis = try {
event.datumBis?.let { LocalDate.parse(it) }
} catch (_: Exception) {
null
},
status = mapStatus(event.status),
ort = event.ort,
vereinName = verein?.name ?: "Unbekannter Verein",
logoUrl = event.logoUrl
)
}
emit(allEvents)
}
override fun getVeranstaltungenByStatus(status: String): Flow<List<VeranstaltungModel>> = flow {
// Aktuell filtern wir noch nicht tief im Store, das Dashboard übernimmt das Filtern des Flows
emit(emptyList())
}
override suspend fun deleteVeranstaltung(veranstalterId: Long, veranstaltungId: Long) {
Store.removeEvent(veranstalterId, veranstaltungId)
}
private fun mapStatus(status: String): VeranstaltungsStatusE {
return when (status) {
"Abgeschlossen" -> VeranstaltungsStatusE.ABGESCHLOSSEN
"In Vorbereitung" -> VeranstaltungsStatusE.IN_PLANUNG
else -> VeranstaltungsStatusE.AKTIV
}
}
}

View File

@ -15,13 +15,17 @@ import at.mocode.frontend.features.device.initialization.di.deviceInitialization
import at.mocode.frontend.features.funktionaer.di.funktionaerModule import at.mocode.frontend.features.funktionaer.di.funktionaerModule
import at.mocode.frontend.features.nennung.di.nennungFeatureModule import at.mocode.frontend.features.nennung.di.nennungFeatureModule
import at.mocode.frontend.features.pferde.di.pferdeModule import at.mocode.frontend.features.pferde.di.pferdeModule
import at.mocode.frontend.features.ping.di.pingFeatureModule
import at.mocode.frontend.features.profile.di.profileModule import at.mocode.frontend.features.profile.di.profileModule
import at.mocode.frontend.features.reiter.di.reiterModule import at.mocode.frontend.features.reiter.di.reiterModule
import at.mocode.frontend.features.turnier.di.turnierFeatureModule
import at.mocode.frontend.features.veranstalter.di.veranstalterModule
import at.mocode.frontend.features.verein.di.vereinFeatureModule import at.mocode.frontend.features.verein.di.vereinFeatureModule
import at.mocode.frontend.features.zns.import.di.znsImportModule import at.mocode.frontend.features.zns.import.di.znsImportModule
import at.mocode.frontend.shell.desktop.data.repository.StoreVeranstaltungRepository
import at.mocode.frontend.shell.desktop.di.desktopModule import at.mocode.frontend.shell.desktop.di.desktopModule
import at.mocode.frontend.features.ping.di.pingFeatureModule import at.mocode.veranstaltung.feature.di.veranstaltungModule
import at.mocode.frontend.features.turnier.di.turnierFeatureModule import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import org.koin.core.context.loadKoinModules import org.koin.core.context.loadKoinModules
@ -45,7 +49,12 @@ fun main() = application {
reiterModule, reiterModule,
funktionaerModule, funktionaerModule,
vereinFeatureModule, vereinFeatureModule,
veranstalterModule,
turnierFeatureModule, turnierFeatureModule,
veranstaltungModule,
module {
single<VeranstaltungRepository> { StoreVeranstaltungRepository() }
},
deviceInitializationModule, deviceInitializationModule,
desktopModule, desktopModule,
) )

View File

@ -70,6 +70,7 @@ fun DesktopMainLayout(
onBack = onBack, onBack = onBack,
onLogout = onLogout, onLogout = onLogout,
isAuthenticated = isAuthenticated, isAuthenticated = isAuthenticated,
isConfigured = onboardingSettings.isConfigured,
connectedPeersCount = connectedPeers.size connectedPeersCount = connectedPeers.size
) )

View File

@ -40,13 +40,14 @@ import at.mocode.frontend.shell.desktop.screens.management.VeranstalterAuswahl
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
import at.mocode.frontend.shell.desktop.screens.veranstaltung.VeranstaltungVerwaltung
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungenScreen
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -76,18 +77,9 @@ fun DesktopContentArea(
// Haupt-Zentrale: Veranstaltung-Verwaltung // Haupt-Zentrale: Veranstaltung-Verwaltung
is AppScreen.VeranstaltungVerwaltung -> { is AppScreen.VeranstaltungVerwaltung -> {
VeranstaltungVerwaltung( VeranstaltungenScreen(
onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }, onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterAuswahl) },
onNewVeranstaltung = { onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
// Wenn wir direkt aus der Übersicht kommen, erst Veranstalter wählen lassen
onNavigate(AppScreen.VeranstalterAuswahl)
},
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) },
onNavigateToFunktionaere = { onNavigate(AppScreen.FunktionaerVerwaltung) },
onNavigateToVeranstalter = { onNavigate(AppScreen.VeranstalterVerwaltung) },
onNavigateToZnsImport = { onNavigate(AppScreen.StammdatenImport) }
) )
} }
@ -231,11 +223,14 @@ fun DesktopContentArea(
} }
is AppScreen.VeranstaltungDetail -> { is AppScreen.VeranstaltungDetail -> {
val repository: at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository = koinInject()
VeranstaltungDetailScreen( VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id, veranstaltungId = currentScreen.id,
repository = repository,
onBack = onBack, onBack = onBack,
onTurnierOeffnen = { tId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tId)) }, onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tId)) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) } onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
onNavigateToVeranstalterProfil = { verId -> onNavigate(AppScreen.VeranstalterDetail(verId)) }
) )
} }
@ -261,11 +256,11 @@ fun DesktopContentArea(
at.mocode.frontend.shell.desktop.data.Store.eventsFor(parent.id).firstOrNull { it.id == evtId } at.mocode.frontend.shell.desktop.data.Store.eventsFor(parent.id).firstOrNull { it.id == evtId }
// bewerbViewModel: BewerbViewModel, nennungViewModel: TurnierNennungViewModel, stammdatenViewModel: TurnierStammdatenViewModel // bewerbViewModel: BewerbViewModel, nennungViewModel: TurnierNennungViewModel, stammdatenViewModel: TurnierStammdatenViewModel
val bewerbViewModel: at.mocode.frontend.features.turnier.presentation.BewerbViewModel = val bewerbViewModel: at.mocode.frontend.features.turnier.presentation.BewerbViewModel =
org.koin.compose.koinInject { parametersOf(currentScreen.turnierId) } koinInject { parametersOf(currentScreen.turnierId) }
val nennungViewModel: at.mocode.frontend.features.turnier.presentation.TurnierNennungViewModel = val nennungViewModel: at.mocode.frontend.features.turnier.presentation.TurnierNennungViewModel =
org.koin.compose.koinInject { parametersOf(currentScreen.turnierId) } koinInject { parametersOf(currentScreen.turnierId) }
val stammdatenViewModel: at.mocode.frontend.features.turnier.presentation.TurnierStammdatenViewModel = val stammdatenViewModel: at.mocode.frontend.features.turnier.presentation.TurnierStammdatenViewModel =
org.koin.compose.koinInject() koinInject()
TurnierDetailScreen( TurnierDetailScreen(
veranstaltungId = evtId, veranstaltungId = evtId,

View File

@ -36,7 +36,7 @@ fun DesktopNavRail(
label = "Logo", label = "Logo",
selected = false, selected = false,
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) }, onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
enabled = true enabled = isConfigured
) )
Spacer(Modifier.height(Dimens.SpacingL)) Spacer(Modifier.height(Dimens.SpacingL))

View File

@ -28,6 +28,7 @@ fun DesktopTopHeader(
onBack: () -> Unit, onBack: () -> Unit,
onLogout: () -> Unit, onLogout: () -> Unit,
isAuthenticated: Boolean, isAuthenticated: Boolean,
isConfigured: Boolean = true,
connectedPeersCount: Int = 0 connectedPeersCount: Int = 0
) { ) {
Surface( Surface(
@ -59,13 +60,16 @@ fun DesktopTopHeader(
// Home Icon als Anker // Home Icon als Anker
IconButton( IconButton(
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) }, onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
modifier = Modifier.size(Dimens.IconSizeM) modifier = Modifier.size(Dimens.IconSizeM),
enabled = isConfigured
) { ) {
Icon( Icon(
imageVector = Icons.Default.Home, imageVector = Icons.Default.Home,
contentDescription = "Home", contentDescription = "Home",
modifier = Modifier.size(Dimens.IconSizeM), modifier = Modifier.size(Dimens.IconSizeM),
tint = MaterialTheme.colorScheme.primary tint = if (isConfigured) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.38f
)
) )
} }

View File

@ -1,195 +0,0 @@
package at.mocode.frontend.shell.desktop.screens.veranstaltung
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.shell.desktop.data.Store
import at.mocode.frontend.shell.desktop.theme.DesktopTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VeranstaltungVerwaltung(
onVeranstaltungOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId
onNewVeranstaltung: () -> Unit,
onNavigateToPferde: () -> Unit,
onNavigateToReiter: () -> Unit,
onNavigateToVereine: () -> Unit,
onNavigateToFunktionaere: () -> Unit,
onNavigateToVeranstalter: () -> Unit,
onNavigateToZnsImport: () -> Unit
) {
LaunchedEffect(Unit) { println("[Screen] VeranstaltungVerwaltung geladen") }
DesktopTheme {
val allVeranstaltungen = remember { Store.allEvents() }
val vereine = Store.vereine
var searchQuery by remember { mutableStateOf("") }
var selectedStatus by remember { mutableStateOf<String?>(null) }
val availableStatuses = remember(allVeranstaltungen) { allVeranstaltungen.map { it.status }.distinct().sorted() }
val filteredVeranstaltungen = remember(allVeranstaltungen, searchQuery, selectedStatus) {
allVeranstaltungen.filter { veranstaltung ->
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
val matchesSearch = veranstaltung.titel.contains(searchQuery, ignoreCase = true) ||
(verein?.name?.contains(searchQuery, ignoreCase = true) ?: false)
val matchesStatus = selectedStatus == null || veranstaltung.status == selectedStatus
matchesSearch && matchesStatus
}.sortedByDescending { it.datumVon }
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
// Header
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Veranstaltungen - verwalten", style = MaterialTheme.typography.headlineMedium)
Button(onClick = onNewVeranstaltung) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Neue Veranstaltung")
}
}
// Filter & Suche
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text("Suche nach Titel oder Verein...") },
modifier = Modifier.weight(1f),
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { searchQuery = "" }) {
Icon(Icons.Default.Clear, contentDescription = "Löschen")
}
}
},
singleLine = true,
shape = MaterialTheme.shapes.medium
)
// Status Filter Chips
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.FilterList, contentDescription = null, tint = Color.Gray)
FilterChip(
selected = selectedStatus == null,
onClick = { selectedStatus = null },
label = { Text("Alle") }
)
availableStatuses.forEach { status ->
FilterChip(
selected = selectedStatus == status,
onClick = { selectedStatus = status },
label = { Text(status) }
)
}
}
}
}
}
// Liste
if (filteredVeranstaltungen.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Default.EventBusy, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.LightGray)
Spacer(Modifier.height(16.dp))
Text("Keine Veranstaltungen gefunden", style = MaterialTheme.typography.bodyLarge, color = Color.Gray)
}
}
} else {
LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxSize()) {
items(filteredVeranstaltungen) { veranstaltung ->
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
VeranstaltungCard(
veranstaltung = veranstaltung,
vereinName = verein?.name ?: "Unbekannter Verein",
onClick = { onVeranstaltungOpen(veranstaltung.veranstalterId, veranstaltung.id) }
)
}
}
}
}
}
}
@Composable
fun VeranstaltungCard(
veranstaltung: at.mocode.frontend.shell.desktop.data.Veranstaltung,
vereinName: String,
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Row(
Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
shape = MaterialTheme.shapes.medium
) {
Icon(
Icons.Default.CalendarToday,
contentDescription = null,
modifier = Modifier.padding(12.dp).size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
}
Column(Modifier.weight(1f)) {
Text(veranstaltung.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(vereinName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.size(14.dp), tint = Color.Gray)
Text(veranstaltung.ort, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
Text("", color = Color.Gray)
Text("${veranstaltung.datumVon} - ${veranstaltung.datumBis ?: ""}", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
}
}
Surface(
color = when (veranstaltung.status) {
"Abgeschlossen" -> Color(0xFFE8F5E9)
"In Vorbereitung" -> Color(0xFFE3F2FD)
else -> MaterialTheme.colorScheme.secondaryContainer
},
shape = MaterialTheme.shapes.small
) {
Text(
veranstaltung.status,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = when (veranstaltung.status) {
"Abgeschlossen" -> Color(0xFF2E7D32)
"In Vorbereitung" -> Color(0xFF1976D2)
else -> MaterialTheme.colorScheme.onSecondaryContainer
}
)
}
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = Color.LightGray)
}
}
}