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:
+8
@@ -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()) }
|
||||
}
|
||||
+16
@@ -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
|
||||
)
|
||||
+10
@@ -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)
|
||||
}
|
||||
+133
-48
@@ -3,65 +3,150 @@ package at.mocode.veranstaltung.feature.presentation
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.Info
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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 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
|
||||
fun VeranstaltungDetailScreen(
|
||||
veranstaltungId: Long,
|
||||
onBack: () -> Unit,
|
||||
onTurnierNeu: () -> Unit,
|
||||
onTurnierOeffnen: (Long) -> Unit,
|
||||
veranstaltungId: Long,
|
||||
repository: VeranstaltungRepository,
|
||||
onBack: () -> Unit,
|
||||
onTurnierNeu: () -> Unit,
|
||||
onTurnierOpen: (Long) -> Unit,
|
||||
onNavigateToVeranstalterProfil: (Long) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Veranstaltung #$veranstaltungId",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
var veranstaltung by remember { mutableStateOf<VeranstaltungModel?>(null) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(veranstaltungId) {
|
||||
isLoading = true
|
||||
// Einfache Abfrage über das Repository (In Phase 14 wird das ViewModel mit StateFlow)
|
||||
val all = repository.getAllVeranstaltungen().firstOrNull()
|
||||
veranstaltung = all?.find { it.id == veranstaltungId }
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
PrimaryTabRow(selectedTabIndex = 0) {
|
||||
Tab(selected = true, onClick = {}, text = { Text("Veranstaltung – Übersicht") })
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
|
||||
// Turniere-Section
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Turniere", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedButton(onClick = onTurnierNeu) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neues Turnier")
|
||||
if (isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val event = veranstaltung
|
||||
if (event == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Veranstaltung #$veranstaltungId nicht gefunden.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
-80
@@ -1,69 +1,39 @@
|
||||
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.combinedClickable
|
||||
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.Event
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.dp
|
||||
import at.mocode.core.domain.model.VeranstaltungsStatusE
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import at.mocode.frontend.core.designsystem.components.MsCard
|
||||
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.
|
||||
*/
|
||||
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".
|
||||
* Veranstaltungs-Übersicht (Einstieg nach Onboarding gemäß ADR-0024).
|
||||
* Zeigt eine Liste aller Veranstaltungen mit Such- und Filterfunktion.
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstaltungenScreen(
|
||||
viewModel: VeranstaltungManagementViewModel = koinInject(),
|
||||
onVeranstaltungNeu: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onVeranstaltungOeffnen: (Long, Long) -> Unit,
|
||||
) {
|
||||
// Später: Echte Daten aus dem ViewModel laden
|
||||
val veranstaltungen = remember {
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
val state = viewModel.state
|
||||
val filteredVeranstaltungen = viewModel.filteredVeranstaltungen
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(Dimens.SpacingL)) {
|
||||
// Header: Titel + Action
|
||||
@@ -80,29 +50,91 @@ fun VeranstaltungenScreen(
|
||||
MsButton(
|
||||
text = "Neue Veranstaltung",
|
||||
onClick = onVeranstaltungNeu
|
||||
// icon = Icons.Default.Add // MsButton unterstützt noch kein Icon im Parameter
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingL))
|
||||
|
||||
if (veranstaltungen.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
"Keine Veranstaltungen gefunden. Legen Sie eine neue an.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
// Suche & Filter (Vision_03 High-Density)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
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 {
|
||||
LazyColumn(
|
||||
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(
|
||||
event = event,
|
||||
onDoubleClick = { onVeranstaltungOeffnen(event.id) }
|
||||
onClick = { onVeranstaltungOeffnen(event.veranstalterId, event.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -110,37 +142,32 @@ fun VeranstaltungenScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun VeranstaltungCard(
|
||||
event: VeranstaltungSimpleUiModel,
|
||||
onDoubleClick: () -> Unit
|
||||
event: VeranstaltungModel,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
MsCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = { /* Einfacher Klick für Selektion, falls gewünscht */ },
|
||||
onDoubleClick = onDoubleClick
|
||||
)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.SpacingS),
|
||||
modifier = Modifier.padding(Dimens.SpacingM),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Platzhalter für Logo
|
||||
// Logo / Icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
.size(48.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Event,
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarToday,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,24 +175,54 @@ fun VeranstaltungCard(
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = event.name,
|
||||
text = event.titel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
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 = event.untertitel,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
"${event.datumVon} - ${event.datumBis ?: ""}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
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 = "${event.ort} | ${event.datum}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
text = event.status.name.lowercase()
|
||||
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user