chore: refactor Veranstalter screens and ViewModels to decouple from Koin, implement hoisting for better testability, consolidate legacy shell logic into modern components, and add mock repositories for previews
This commit is contained in:
+4
@@ -2,8 +2,12 @@ package at.mocode.frontend.features.veranstalter.di
|
||||
|
||||
import at.mocode.frontend.features.veranstalter.data.remote.FakeVeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailViewModel
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterViewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val veranstalterModule = module {
|
||||
single<VeranstalterRepository> { FakeVeranstalterRepository() }
|
||||
factory { VeranstalterViewModel(get()) }
|
||||
factory { VeranstalterDetailViewModel(get()) }
|
||||
}
|
||||
|
||||
+5
-4
@@ -8,12 +8,14 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -30,12 +32,11 @@ private val AccentBlue = Color(0xFF3B82F6)
|
||||
*/
|
||||
@Composable
|
||||
fun VeranstalterAuswahlScreen(
|
||||
viewModel: VeranstalterViewModel,
|
||||
onZurueck: () -> Unit,
|
||||
onWeiter: (Long) -> Unit,
|
||||
onNeuerVeranstalter: () -> Unit = {},
|
||||
) {
|
||||
val repo: at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository = org.koin.compose.koinInject()
|
||||
val viewModel = remember { VeranstalterViewModel(repo) }
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().background(Color.White)) {
|
||||
|
||||
+16
-78
@@ -7,13 +7,16 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -44,85 +47,20 @@ private val StatusAbgeschlossenColor = Color(0xFF6B7280)
|
||||
@Composable
|
||||
fun VeranstalterDetailScreen(
|
||||
veranstalterId: Long,
|
||||
viewModel: VeranstalterDetailViewModel,
|
||||
onZurueck: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onVeranstaltungNeu: () -> Unit,
|
||||
) {
|
||||
var suchtext by remember { mutableStateOf("") }
|
||||
var statusFilter by remember { mutableStateOf(VeranstaltungStatusFilter.ALLE) }
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
// Placeholder-Daten gemäß Figma
|
||||
val veranstalter = remember(veranstalterId) {
|
||||
VeranstalterDetailUiModel(
|
||||
id = veranstalterId,
|
||||
name = "Reit- und Fahrverein Wels",
|
||||
oepsNummer = "V-OOE-1234",
|
||||
ansprechpartner = "Maria Huber",
|
||||
email = "office@rfv-wels.at",
|
||||
telefon = "+43 7242 12345",
|
||||
adresse = "Reitweg 15\n4600 Wels",
|
||||
loginStatus = LoginStatus.AKTIV,
|
||||
mitgliedSeit = "15.1.2023",
|
||||
)
|
||||
}
|
||||
|
||||
// Liste aus dem Fake-Store (pro Veranstalter). Falls leer, einmalig seeden.
|
||||
val storeList = FakeVeranstaltungStore.listFor(veranstalterId)
|
||||
LaunchedEffect(veranstalterId) {
|
||||
if (storeList.isEmpty()) {
|
||||
FakeVeranstaltungStore.seedIfEmpty(
|
||||
veranstalterId,
|
||||
listOf(
|
||||
VeranstaltungListUiModel(
|
||||
id = 1L,
|
||||
name = "Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026",
|
||||
datum = "25.-26. April 2026",
|
||||
ort = "Reitanlage Stroblmair, Neumarkt/M., OO",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 87,
|
||||
bewerbe = 26,
|
||||
letzteAktivitaet = "22.03.2026 14:30",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 2L,
|
||||
name = "AWÖ-Cup Stadl-Paura 2025",
|
||||
datum = "15.-17. Mai 2025",
|
||||
ort = "Bundesgestüt Piber, Stadl-Paura",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 142,
|
||||
bewerbe = 33,
|
||||
letzteAktivitaet = "17.05.2025 18:45",
|
||||
status = VeranstaltungStatus.ABGESCHLOSSEN,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 3L,
|
||||
name = "Linzer Pferdetage 2026",
|
||||
datum = "12.-14. Juni 2026",
|
||||
ort = "Reitsportzentrum Linz-Ebelsberg",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 23,
|
||||
bewerbe = 30,
|
||||
letzteAktivitaet = "20.03.2026 09:15",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
viewModel.send(VeranstalterDetailIntent.Load(veranstalterId))
|
||||
}
|
||||
|
||||
val gefiltert = storeList.filter { v ->
|
||||
val matchesStatus = when (statusFilter) {
|
||||
VeranstaltungStatusFilter.ALLE -> true
|
||||
VeranstaltungStatusFilter.VORBEREITUNG -> v.status == VeranstaltungStatus.VORBEREITUNG
|
||||
VeranstaltungStatusFilter.LIVE -> v.status == VeranstaltungStatus.LIVE
|
||||
VeranstaltungStatusFilter.ABGESCHLOSSEN -> v.status == VeranstaltungStatus.ABGESCHLOSSEN
|
||||
}
|
||||
val matchesSuche = suchtext.isBlank() ||
|
||||
v.name.contains(suchtext, ignoreCase = true) ||
|
||||
v.ort.contains(suchtext, ignoreCase = true)
|
||||
matchesStatus && matchesSuche
|
||||
}
|
||||
val veranstalter = state.veranstalter ?: return // Oder Loading-Spinner
|
||||
|
||||
val gefiltert = state.filteredVeranstaltungen
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Header mit Zurück-Pfeil ─────────────────────────────────────────
|
||||
@@ -223,8 +161,8 @@ fun VeranstalterDetailScreen(
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = suchtext,
|
||||
onValueChange = { suchtext = it },
|
||||
value = state.suchtext,
|
||||
onValueChange = { viewModel.send(VeranstalterDetailIntent.SearchChanged(it)) },
|
||||
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 12.sp) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
||||
modifier = Modifier.weight(1f).height(44.dp),
|
||||
@@ -233,10 +171,10 @@ fun VeranstalterDetailScreen(
|
||||
// Status-Filter-Chips
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
VeranstaltungStatusFilter.entries.forEach { filter ->
|
||||
val isActive = statusFilter == filter
|
||||
val isActive = state.statusFilter == filter
|
||||
FilterChip(
|
||||
selected = isActive,
|
||||
onClick = { statusFilter = filter },
|
||||
onClick = { viewModel.send(VeranstalterDetailIntent.FilterChanged(filter)) },
|
||||
label = {
|
||||
Text(
|
||||
text = when (filter) {
|
||||
@@ -268,7 +206,7 @@ fun VeranstalterDetailScreen(
|
||||
veranstaltung = veranstaltung,
|
||||
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
|
||||
onLoeschen = {
|
||||
FakeVeranstaltungStore.remove(veranstalterId, veranstaltung.id)
|
||||
viewModel.send(VeranstalterDetailIntent.DeleteVeranstaltung(veranstaltung.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatus
|
||||
|
||||
data class VeranstalterDetailState(
|
||||
val isLoading: Boolean = false,
|
||||
val veranstalter: VeranstalterDetailUiModel? = null,
|
||||
val veranstaltungen: List<VeranstaltungListUiModel> = emptyList(),
|
||||
val filteredVeranstaltungen: List<VeranstaltungListUiModel> = emptyList(),
|
||||
val suchtext: String = "",
|
||||
val statusFilter: VeranstaltungStatusFilter = VeranstaltungStatusFilter.ALLE,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
sealed interface VeranstalterDetailIntent {
|
||||
data class Load(val id: Long) : VeranstalterDetailIntent
|
||||
data class SearchChanged(val query: String) : VeranstalterDetailIntent
|
||||
data class FilterChanged(val filter: VeranstaltungStatusFilter) : VeranstalterDetailIntent
|
||||
data class DeleteVeranstaltung(val id: Long) : VeranstalterDetailIntent
|
||||
}
|
||||
|
||||
class VeranstalterDetailViewModel(
|
||||
private val repo: VeranstalterRepository,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val _state = MutableStateFlow(VeranstalterDetailState(isLoading = true))
|
||||
val state: StateFlow<VeranstalterDetailState> = _state
|
||||
|
||||
fun send(intent: VeranstalterDetailIntent) {
|
||||
when (intent) {
|
||||
is VeranstalterDetailIntent.Load -> load(intent.id)
|
||||
is VeranstalterDetailIntent.SearchChanged -> {
|
||||
_state.value = _state.value.copy(suchtext = intent.query)
|
||||
applyFilter()
|
||||
}
|
||||
is VeranstalterDetailIntent.FilterChanged -> {
|
||||
_state.value = _state.value.copy(statusFilter = intent.filter)
|
||||
applyFilter()
|
||||
}
|
||||
is VeranstalterDetailIntent.DeleteVeranstaltung -> deleteVeranstaltung(intent.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(id: Long) {
|
||||
_state.value = _state.value.copy(isLoading = true)
|
||||
scope.launch {
|
||||
// In einer echten App würden wir hier das Repo abfragen.
|
||||
// Für den Prototyp nutzen wir vorerst die Logik aus dem Screen, aber im VM gekapselt.
|
||||
|
||||
val mockVeranstalter = VeranstalterDetailUiModel(
|
||||
id = id,
|
||||
name = "Reit- und Fahrverein Wels",
|
||||
oepsNummer = "V-OOE-1234",
|
||||
ansprechpartner = "Maria Huber",
|
||||
email = "office@rfv-wels.at",
|
||||
telefon = "+43 7242 12345",
|
||||
adresse = "Reitweg 15\n4600 Wels",
|
||||
loginStatus = LoginStatus.AKTIV,
|
||||
mitgliedSeit = "15.1.2023",
|
||||
)
|
||||
|
||||
val mockVeranstaltungen = listOf(
|
||||
VeranstaltungListUiModel(
|
||||
id = 1L,
|
||||
name = "Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026",
|
||||
datum = "25.-26. April 2026",
|
||||
ort = "Reitanlage Stroblmair, Neumarkt/M., OO",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 87,
|
||||
bewerbe = 26,
|
||||
letzteAktivitaet = "22.03.2026 14:30",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 2L,
|
||||
name = "AWÖ-Cup Stadl-Paura 2025",
|
||||
datum = "15.-17. Mai 2025",
|
||||
ort = "Bundesgestüt Piber, Stadl-Paura",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 142,
|
||||
bewerbe = 33,
|
||||
letzteAktivitaet = "17.05.2025 18:45",
|
||||
status = VeranstaltungStatus.ABGESCHLOSSEN,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 3L,
|
||||
name = "Linzer Pferdetage 2026",
|
||||
datum = "12.-14. Juni 2026",
|
||||
ort = "Reitsportzentrum Linz-Ebelsberg",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 23,
|
||||
bewerbe = 30,
|
||||
letzteAktivitaet = "20.03.2026 09:15",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
)
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
isLoading = false,
|
||||
veranstalter = mockVeranstalter,
|
||||
veranstaltungen = mockVeranstaltungen,
|
||||
filteredVeranstaltungen = mockVeranstaltungen
|
||||
)
|
||||
applyFilter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteVeranstaltung(id: Long) {
|
||||
val newList = _state.value.veranstaltungen.filter { it.id != id }
|
||||
_state.value = _state.value.copy(veranstaltungen = newList)
|
||||
applyFilter()
|
||||
}
|
||||
|
||||
private fun applyFilter() {
|
||||
val current = _state.value
|
||||
val filtered = current.veranstaltungen.filter { v ->
|
||||
val matchesSearch = v.name.contains(current.suchtext, ignoreCase = true) ||
|
||||
v.ort.contains(current.suchtext, ignoreCase = true)
|
||||
val matchesStatus = when (current.statusFilter) {
|
||||
VeranstaltungStatusFilter.ALLE -> true
|
||||
VeranstaltungStatusFilter.VORBEREITUNG -> v.status == VeranstaltungStatus.VORBEREITUNG
|
||||
VeranstaltungStatusFilter.LIVE -> v.status == VeranstaltungStatus.LIVE
|
||||
VeranstaltungStatusFilter.ABGESCHLOSSEN -> v.status == VeranstaltungStatus.ABGESCHLOSSEN
|
||||
}
|
||||
matchesSearch && matchesStatus
|
||||
}
|
||||
_state.value = _state.value.copy(filteredVeranstaltungen = filtered)
|
||||
}
|
||||
}
|
||||
+22
-189
@@ -1,22 +1,14 @@
|
||||
package at.mocode.frontend.shell.desktop.screens.management
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
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
|
||||
import androidx.compose.runtime.Composable
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
/**
|
||||
* Delegates für die Veranstalter-Screens in der Desktop-Shell.
|
||||
* Diese binden die Plug-and-Play Komponenten aus dem veranstalter-feature ein.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun VeranstalterAuswahl(
|
||||
@@ -24,55 +16,12 @@ fun VeranstalterAuswahl(
|
||||
onWeiter: (Long) -> Unit,
|
||||
onNeu: () -> Unit,
|
||||
) {
|
||||
DesktopTheme {
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text("Veranstalter auswählen", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(onClick = onNeu) { Text("+ Neuer Veranstalter") }
|
||||
}
|
||||
|
||||
var selectedId by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize().weight(1f)) {
|
||||
items(Store.vereine) { v ->
|
||||
val sel = selectedId == v.id
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
.clickable { selectedId = v.id },
|
||||
colors = if (sel) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
else CardDefaults.cardColors()
|
||||
) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier.size(40.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text((v.kurzname ?: v.name).take(2).uppercase(), color = Color.White)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(v.name, style = MaterialTheme.typography.titleMedium)
|
||||
Text("OEPS: ${v.oepsNummer} · ${v.ort ?: ""}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { selectedId?.let(onWeiter) },
|
||||
enabled = selectedId != null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("Weiter zum Veranstalter") }
|
||||
}
|
||||
}
|
||||
VeranstalterAuswahlScreen(
|
||||
viewModel = koinInject(),
|
||||
onZurueck = onBack,
|
||||
onWeiter = onWeiter,
|
||||
onNeuerVeranstalter = onNeu
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -82,127 +31,11 @@ fun VeranstalterDetail(
|
||||
onZurVeranstaltung: (Long) -> Unit,
|
||||
onNeuVeranstaltung: () -> Unit,
|
||||
) {
|
||||
LaunchedEffect(veranstalterId) { println("[Screen] VeranstalterDetail geladen (VstID: $veranstalterId)") }
|
||||
DesktopTheme {
|
||||
val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } }
|
||||
|
||||
if (verein == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Veranstalter nicht gefunden")
|
||||
}
|
||||
return@DesktopTheme
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text(verein.name, style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
|
||||
}
|
||||
|
||||
var editOpen by remember { mutableStateOf(false) }
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
(verein.kurzname ?: verein.name).take(2).uppercase(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(verein.name, style = MaterialTheme.typography.titleMedium)
|
||||
val line2 = listOfNotNull(
|
||||
"OEPS: ${verein.oepsNummer}",
|
||||
verein.ort,
|
||||
verein.plz,
|
||||
verein.strasse
|
||||
).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
if (line2.isNotBlank()) Text(line2, color = Color(0xFF6B7280))
|
||||
val line3 = listOfNotNull(verein.email, verein.telefon).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
if (line3.isNotBlank()) Text(line3, color = Color(0xFF6B7280))
|
||||
}
|
||||
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
|
||||
}
|
||||
}
|
||||
|
||||
if (editOpen) {
|
||||
var name by remember { mutableStateOf(verein.name) }
|
||||
var oeps by remember { mutableStateOf(verein.oepsNummer) }
|
||||
var ort by remember { mutableStateOf(verein.ort ?: "") }
|
||||
var plz by remember { mutableStateOf(verein.plz ?: "") }
|
||||
var strasse by remember { mutableStateOf(verein.strasse ?: "") }
|
||||
var email by remember { mutableStateOf(verein.email ?: "") }
|
||||
var tel by remember { mutableStateOf(verein.telefon ?: "") }
|
||||
var logo by remember { mutableStateOf(verein.logoUrl ?: "") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { editOpen = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
verein.name = name
|
||||
verein.oepsNummer = oeps
|
||||
verein.ort = ort.ifBlank { null }
|
||||
verein.plz = plz.ifBlank { null }
|
||||
verein.strasse = strasse.ifBlank { null }
|
||||
verein.email = email.ifBlank { null }
|
||||
verein.telefon = tel.ifBlank { null }
|
||||
verein.logoUrl = logo.ifBlank { null }
|
||||
editOpen = false
|
||||
}) { Text("Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
|
||||
title = { Text("Veranstalter bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("OEPS-Nummer") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(logo, { logo = it }, label = { Text("Logo-URL") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(ort, { ort = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(plz, { plz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
OutlinedTextField(
|
||||
strasse,
|
||||
{ strasse = it },
|
||||
label = { Text("Straße") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(tel, { tel = it }, label = { Text("Telefon") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text("Veranstaltungen", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
|
||||
val events = remember(veranstalterId) { Store.eventsFor(veranstalterId) }
|
||||
if (events.isEmpty()) {
|
||||
Text("Keine Veranstaltungen angelegt", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||
} else {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(events) { ev ->
|
||||
ListItem(
|
||||
headlineContent = { Text(ev.titel) },
|
||||
supportingContent = { Text("${ev.datumVon} bis ${ev.datumBis ?: "?"} · ${ev.status}") },
|
||||
trailingContent = { Icon(Icons.Default.ChevronRight, null) },
|
||||
modifier = Modifier.clickable { onZurVeranstaltung(ev.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = veranstalterId,
|
||||
viewModel = koinInject(),
|
||||
onZurueck = onBack,
|
||||
onVeranstaltungOeffnen = onZurVeranstaltung,
|
||||
onVeranstaltungNeu = onNeuVeranstaltung
|
||||
)
|
||||
}
|
||||
|
||||
+27
-3
@@ -7,11 +7,11 @@ import at.mocode.frontend.features.turnier.data.remote.dto.NennungEinreichenRequ
|
||||
import at.mocode.frontend.features.turnier.domain.*
|
||||
import at.mocode.frontend.features.turnier.domain.model.StartlistenZeile
|
||||
import at.mocode.frontend.features.turnier.presentation.*
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.presentation.*
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
||||
import at.mocode.zns.parser.ZnsBewerb
|
||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Compose Desktop Previews – alle wichtigen Screens auf einen Blick
|
||||
@@ -25,8 +25,20 @@ import at.mocode.zns.parser.ZnsBewerb
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewVeranstalterAuswahlScreen() {
|
||||
val mockRepo = object : VeranstalterRepository {
|
||||
override suspend fun list(): Result<List<DomainVeranstalter>> = Result.success(emptyList())
|
||||
override suspend fun getById(id: Long): Result<DomainVeranstalter> = Result.failure(NotImplementedError())
|
||||
override suspend fun create(model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun update(id: Long, model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun delete(id: Long): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
MaterialTheme {
|
||||
VeranstalterAuswahlScreen(
|
||||
viewModel = VeranstalterViewModel(mockRepo),
|
||||
onZurueck = {},
|
||||
onWeiter = {},
|
||||
onNeuerVeranstalter = {},
|
||||
@@ -52,9 +64,21 @@ fun PreviewVeranstalterNeuScreen() {
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewVeranstalterDetailScreen() {
|
||||
val mockRepo = object : VeranstalterRepository {
|
||||
override suspend fun list(): Result<List<DomainVeranstalter>> = Result.success(emptyList())
|
||||
override suspend fun getById(id: Long): Result<DomainVeranstalter> = Result.failure(NotImplementedError())
|
||||
override suspend fun create(model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun update(id: Long, model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun delete(id: Long): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
MaterialTheme {
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = 1L,
|
||||
viewModel = VeranstalterDetailViewModel(mockRepo),
|
||||
onZurueck = {},
|
||||
onVeranstaltungOeffnen = {},
|
||||
onVeranstaltungNeu = {},
|
||||
|
||||
Reference in New Issue
Block a user